RFC: Add Store.send
and Store.withState
#2223
Replies: 8 comments 14 replies
-
Regarding let currentState = store.state
// Do something with the current state...
// Pluck the current count out of the store:
let count = store.scope(state: \.count).state
// or
let count = store.state.count I'm probably trivializing something, but I don't quite understand the need to open the closure context here. store.withState { currentState in
// Do something with the current state...
} |
Beta Was this translation helpful? Give feedback.
-
Is this only in regards to |
Beta Was this translation helpful? Give feedback.
-
I'm DIGGING this new functionality! Works out-of-the-box; very elegant to have a way of accessing within a closure and also pluck it out! When creating a UICollectionView instance using cell registration API, the plucking makes getting state trivial: - super.init(collectionView: collectionView) { [weak store] collectionView, indexPath, itemId in
+ super.init(collectionView: collectionView) {
+ [weak store] collectionView, indexPath, itemId in
guard
let store,
- let item = ViewStore(store, observe: \.section.items[id: itemId]).state
+ let item = store.withState(\.section.items[id: itemId])
else {
return nil
} This also will make it's way in all the CollectionViewDataSource, CollectionViewDelegate classes I have throughout my codebase too. |
Beta Was this translation helpful? Give feedback.
-
One question I have is whether the long-term vision is to phase out the concept of scoping and transforming business state to view state and that store really becomes the single source of truth and everything is just a mere reflection of that. Although I see how people find ViewStore to be really annoying, I've really enjoyed it because of the high-level scoping that it provides. Practically all view domains in my codebase do something like this: public class FooController: UIViewController {
public let store: StoreOf<Foo>
public var viewStore: ViewStore<ViewState, Action.ViewAction>
public struct ViewState: Equatable {
var buttonColor: UIColor
public init(state: Foo.State) {
switch state.houseTemp {
case .HOT:
self.buttonColor = UIColor.red
case .normal:
self.buttonColor = UIColor.lightGray
case .cold:
self.buttonColor = UIColor.systemBlue
}
}
}
public func setupBindings() {
viewStore.publisher.buttonColor
.removeDuplicates(by: ==)
.assignNoRetain(\UIButton.backgroundColor, with: myButton)
.store(in: &cancellables)
}
To me, this kind of logic does not belong anywhere near a reducer because it is 100% view logic. When it comes to testing, then all I have to do is just create a FooViewController.ViewState, pass it some parameters, and make assertions. No need to do anything with stores there. I'm wondering how (or if) view logic will be kept in its own sandbox while the business logic remains elsewhere during these attempts of phasing out ViewStore. |
Beta Was this translation helpful? Give feedback.
-
Second question is whether the path forward would mean I can no longer scope an action to a domain like ViewAction à la Krzysztof Zablocki's TCA boundaries. I really dig being able to just run This is of course is NOT a big deal at all. If I lose this feature, then I'll just write myself a SwiftLint rule that catches instances when I'm not passing a |
Beta Was this translation helpful? Give feedback.
-
I honestly do not see the need for this change quite yet. I understand |
Beta Was this translation helpful? Give feedback.
-
Hi all! Thanks again for participating in the discussion. Given the general positive response, and the vision for the future of the library, we think this intermediate change is worth making before 1.0, so we're going to merge the associated branch now. |
Beta Was this translation helpful? Give feedback.
-
Now that the store's publisher is exposed and ViewStore is on it's way out the door, does it make more sense now to expose a store's state as read-only properties? Or perhaps this could just be a UIKit-specific feature LOLOL |
Beta Was this translation helpful? Give feedback.
-
Ever since its introduction, the Composable Architecture has provided one way of accessing store state and sending store actions: through a
ViewStore
. The view store abstraction made a ton of sense at the time for SwiftUI, because we needed an object that could be observed in the view when state changes, and this object could not simply be the store itself, because stores typically contain much more state than its associated view needs to do its job, and this leads to unnecessary view computations and performance problems.The view store is not a lightweight abstraction. It requires a noisy amount of code to create one, which has grown noisier over time, and poses ergonomic issues when you just want to send an action and no view store is available:
The act of creating a view store is also not lightweight: it sets up a Combine subscription for SwiftUI to observe.
To address these issues, we introduced an RFC earlier this year to lukewarm response: #1946
The solution didn’t seem to be quite right.
Well, now that WWDC23 has come and gone and we’ve taken the Observation framework for a spin, we have a much clearer picture of the future of TCA, so we’d like to propose some incremental steps we can take with the library to make it more ergonomic today, while keeping sight of the future.
🪦 R.I.P. ViewStore, 2020–2023
In particular, view stores are not long for this world. With Observability we can forgo the view store abstraction and simply access state directly on the store, and the view will track changes to the properties it accesses and re-render only when those properties change. And so it makes sense to also send actions directly to the store.
To prepare for this future we would like to introduce a couple changes that make the Composable Architecture a bit more lenient, and ergonomic, today:
Store.send
We will be adding a
send
method directly to the store so that you can send actions when a view store is not available. Rather than jumping through hoops to create and throw away a view store just to send an action, simply send the action directly to the store:This admittedly muddies the waters a bit. There are now two ways to send actions to the store. Our recommendation is to prefer sending actions to view stores when they are available and fall back to sending actions to stores only when necessary. In the future world of Observability this muddiness gets cleaned up as the view store is retired.
Store.withState
We would also like to provide a lightweight way to access the current state of a store without the overhead of a view store.
Note that accessing state this way does not perform any observation at all. You are only getting a snapshot of state at a specific time, and if you do so in a SwiftUI view you should not expect the view to update when this state changes.
Future directions
These are two small steps towards to future, but there are obvious areas of the library to address.
Store.send
vs.ViewStore.send
At the moment we are only adding a single
Store.send
method. There are many versions ofViewStore.send
that layer on things like SwiftUI animation, async-await, etc. While we plan on surfacing this functionality onStore
in the future, you can still access this functionality today onViewStore
.UIKit View Stores
We have always felt that view stores were a bit forced onto UIKit, and they never fit quite as well. We reasoned that having a single unified way of interacting with state and actions made it worth it, but now that it’s all going away, it probably makes sense to distance the Composable Architecture’s UIKit integration from the view store concept entirely. Instead of creating a view store to access state, we could provide a
.publisher
directly onStore
, and allow folks to usewithState
when accessing state in an unobserved context.Try it out today
We just pushed a branch off of
main
,store-send-and-with-state
, and opened a pull request here. Please give it a try in your applications, and let us know if you have any feedback!Beta Was this translation helpful? Give feedback.
All reactions