How do you best pass scoped stores thru multiple layers?
I often see myself wanting to pass a scoped store to other views from the body, so that I can keep drilling it down even further in those views and only consume subsets of the state.
But I started to realize that it seems to be pretty much an anti-pattern to inject
Store into Views and hold on to them in a property. (Beyond what
WithViewStore does for example: turn it into a
ViewStore in the initializer right away and only hold on to the view store.)
That is at least as long as not all of these are given:
- the view is declared as
- the store property is explicitly excluded from the equality implementation
- and the view is used with the
Why? Because that would forgo all optimizations around drilling down to a minimal view state and removing duplicates, as it forces SwiftUI to re-evaluate the body whenever the store reference changes. SwiftUI's diffing relies internally on reference identity for reference-typed properties on views (when they have no property wrapper).
This might be fine as long as you're sharing your "main store" / single source of truth all around, which will be always the same reference (sounds like overall a bad idea tho, bc it forgos the C in TCA), but becomes problematic at the latest when you're scoping stores. When scoping stores, you always get a fresh new store instance whenever
Store.scope is called. So when it's called from a
View.body and injected into other
Views, then that would effectively mean that whenever the parent in such a scenario gets its body re-evaluated, the whole affected subview hierarchy will have to be re-evaluated as well.
(Note: It seems that this is not usually the case with "vanilla" SwiftUI. Parent bodies can be re-evaluated independently. The children will be only re-evaluated, when SwiftUI decides per its diffing that the children at hand are actually different.)
So that also means scoping from the
View.body or a view's computed variables would be pretty much an anti-pattern as well – if and only if you're planning to use this scope beyond turning it into a
ViewStore right away. (e.g. via
So where do I scope now? If I scope my store in the initializer, then I need to hold on to the scoped store somewhere – a stored property? Well, now I've run into the very same problem again.
Sooo does this eventually mean, I should only really inject scoped stores into equatable views which ignore the store? At least if I care about the diffing performance and want to keep the number of body calls down. I'm aware that Swift structs are relatively cheap. I'm not quite sure how much overhead TCA adds to that with its internal combine operations and Store/ViewStore allocations.
For my project I'm observing an unreasonable number of updates and wish to cut down on that, to improve performance on the one hand, but also just alone to improve debugging experience on the other hand.