Non-TCA version
\nimport SwiftUI\n\n// MARK: - List\n@MainActor class ListStoreNonTCA: ObservableObject {\n struct State {\n var cellStores: [CellStoreNonTCA] = []\n }\n @Published var state: State\n init() {\n self.state = State()\n }\n \n func addTapped() {\n let newId = state.cellStores.count\n state.cellStores.append(.init(state: .init(id: newId)))\n }\n}\n\nstruct ContentViewNonTCA: View {\n @ObservedObject var store = ListStoreNonTCA()\n \n var body: some View {\n List {\n Button(\"Add row\") {\n store.addTapped()\n }\n\n ForEach(store.state.cellStores, id: \\.state.id) { cellStore in\n CellViewNonTCA(store: cellStore)\n }\n }\n .listStyle(.plain)\n }\n}\n\n// MARK: - Cell\n@MainActor final class CellStoreNonTCA: ObservableObject {\n struct State {\n let id: Int\n var text: String? = nil\n }\n @Published var state: State\n \n init(state: State) {\n self.state = state\n }\n \n func onAppear() {\n Task {\n state.text = \"Content fetched at \\(Date())\"\n }\n }\n}\n\nstruct CellViewNonTCA: View {\n @ObservedObject var store: CellStoreNonTCA\n\n var body: some View {\n let _ = print(\"rendering cell \\(store.state.id)\")\n\n VStack {\n Text(store.state.text ?? \"\")\n .onAppear { store.onAppear() }\n }\n }\n}\n\n#Preview {\n ContentViewNonTCA()\n}
Thanks a lot!
","upvoteCount":2,"answerCount":1,"acceptedAnswer":{"@type":"Answer","text":"Hey @ba01ei , I believe your question relates to another one that was asked recently. You might find this thread helpful:
\n#2973
-
When I have a List backed by a TCA store, and the each cell in the List has its own store, which is scoped out from the List's store using Is there a way to avoid rendering list cells that didn't have any updates, like the non-TCA version? Did I miss something about TCA & the stores/reducers are not set up correctly? Here is the demo and code: over-render.mov(In this case, when there are 5 rows and tapping the "Add row" button should only render cell 6, not 1-5) TCA version: import ComposableArchitecture
import SwiftUI
// MARK: - List
@Reducer
struct ListTCA {
@ObservableState
struct State: Equatable {
var cells: IdentifiedArrayOf<CellTCA.State> = []
}
enum Action {
case addTapped
case cellAction(IdentifiedActionOf<CellTCA>)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .addTapped:
let newId = state.cells.count
state.cells.append(.init(id: newId))
return .none
case .cellAction:
return .none
}
}
.forEach(\.cells, action: \.cellAction) {
CellTCA()
}
}
}
struct ContentViewTCA: View {
let store = Store(initialState: .init()) {
ListTCA()
}
var body: some View {
List {
Button("Add row") {
store.send(.addTapped)
}
ForEach(store.scope(state: \.cells, action: \.cellAction)) { cellStore in
CellView(store: cellStore)
}
}
.listStyle(.plain)
}
}
// MARK: - Cell
@Reducer
struct CellTCA {
@ObservableState
struct State: Equatable, Identifiable {
let id: Int
var text: String? = nil
var cellId: String {
id.description
}
}
enum Action {
case onAppear
case contentFetched(String)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .onAppear:
// asynchronously load content
return .run { [id = state.id] send in
await send(.contentFetched("Content of \(id) fetched at \(Date())"))
}
case .contentFetched(let content):
state.text = content
return .none
}
}
}
}
struct CellView: View {
let store: StoreOf<CellTCA>
var body: some View {
let _ = print("rendering cell \(store.cellId)")
VStack {
Text(store.text ?? "")
.onAppear { store.send(.onAppear) }
}
}
}
#Preview {
ContentViewTCA()
} Non-TCA version import SwiftUI
// MARK: - List
@MainActor class ListStoreNonTCA: ObservableObject {
struct State {
var cellStores: [CellStoreNonTCA] = []
}
@Published var state: State
init() {
self.state = State()
}
func addTapped() {
let newId = state.cellStores.count
state.cellStores.append(.init(state: .init(id: newId)))
}
}
struct ContentViewNonTCA: View {
@ObservedObject var store = ListStoreNonTCA()
var body: some View {
List {
Button("Add row") {
store.addTapped()
}
ForEach(store.state.cellStores, id: \.state.id) { cellStore in
CellViewNonTCA(store: cellStore)
}
}
.listStyle(.plain)
}
}
// MARK: - Cell
@MainActor final class CellStoreNonTCA: ObservableObject {
struct State {
let id: Int
var text: String? = nil
}
@Published var state: State
init(state: State) {
self.state = state
}
func onAppear() {
Task {
state.text = "Content fetched at \(Date())"
}
}
}
struct CellViewNonTCA: View {
@ObservedObject var store: CellStoreNonTCA
var body: some View {
let _ = print("rendering cell \(store.state.id)")
VStack {
Text(store.state.text ?? "")
.onAppear { store.onAppear() }
}
}
}
#Preview {
ContentViewNonTCA()
} Thanks a lot! |
Beta Was this translation helpful? Give feedback.
-
I figured out a way to avoid the re-rendering issue, basically if I just pass the id to each cell and let each cell view create their own store and keep it as a But this kinda feels a little anti-pattern, and we lost the ability to communicate between parent and child stores: Screen.Recording.2025-05-19.at.11.30.38.AM.movimport ComposableArchitecture
import SwiftUI
// MARK: - List
@Reducer
struct ListTCA {
@ObservableState
struct State: Equatable {
var cells: IdentifiedArrayOf<CellTCA.State> = []
}
enum Action {
case addTapped
case cellAction(IdentifiedActionOf<CellTCA>)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .addTapped:
let newId = state.cells.count
state.cells.append(.init(id: newId))
return .none
case .cellAction:
return .none
}
}
// .forEach(\.cells, action: \.cellAction) {
// CellTCA()
// }
}
}
struct ContentViewTCA: View {
let store = Store(initialState: .init()) {
ListTCA()
}
var body: some View {
List {
Button("Add row") {
store.send(.addTapped)
}
// ForEach(store.scope(state: \.cells, action: \.cellAction)) { cellStore in
ForEach(store.cells) { cell in
CellView(id: cell.id)
}
}
.listStyle(.plain)
}
}
// MARK: - Cell
@Reducer
struct CellTCA {
@ObservableState
struct State: Equatable, Identifiable {
let id: Int
var text: String? = nil
var cellId: String {
id.description
}
}
enum Action {
case onAppear
case contentFetched(String)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .onAppear:
// asynchronously load content
return .run { [id = state.id] send in
await send(.contentFetched("Content of \(id) fetched at \(Date())"))
}
case .contentFetched(let content):
state.text = content
return .none
}
}
}
}
struct CellView: View {
@StateObject var store: StoreOf<CellTCA>
init(id: Int) {
// this way fixes the re-render issue:
_store = .init(wrappedValue: Store(initialState: .init(id: id), reducer: {
CellTCA()
}))
}
var body: some View {
let _ = print("rendering cell \(store.cellId)")
VStack {
Text(store.text ?? "")
.onAppear { store.send(.onAppear) }
}
}
}
#Preview {
ContentViewTCA()
} I also tried using scoping to pass the store from the List to the Cell but storing the store as |
Beta Was this translation helpful? Give feedback.
Hey @ba01ei , I believe your question relates to another one that was asked recently. You might find this thread helpful:
#2973