-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Unsafe access to store state and actions. #1946
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the general direction! Will let others weigh in on any API ideas.
"Safe" has a very particular meaning in Swift, namely "memory safe". It doesn't seem like a good idea to conflate the safety of Swift itself with a TCA-specific construct. Perhaps we can think of a TCA-specific term for this sort of inefficient |
@jshier that's definitely true, but we took inspiration for this API from Swift's |
Eh, I'd guess unchecked continuations could lead to memory corruption if their resumption wasn't monitored, since you could have a thread safety issue. I suppose the definition from the language can now be expanded to "thread safe" as well. In either case, this API doesn't seem unsafe by any definition. At worst it's inefficient. You could really stretch the term and consider it unsafe because large, unscoped observation may trigger latent SwiftUI bugs, but you can't really use it for that here, can you? Additionally, since the "create local |
Perhaps "unobserved" instead of "unsafe"? Out of curiosity why do we still need a store.withUnsafeStore { $0.send(.addButtonTapped) } // 😭
store.send(.addButtonTapped) // 🤷♂️ |
Yeah, if this converts to a |
Or is this a first step towards eliminating |
It's a reasonable question, and honestly it's funny how difficult it is to answer directly. Here's my best shot. If we step back a bit, the main reason for This is in stark contrast to stores which are meant to be passed along to child views, although they are usually first scoped. This is what makes it hard to combine both of these concepts into one package. For example, many people want scope actions specifically for a view so that the view can't send "internal" or "effect" actions. How would you do that without a view store? If you scope the All of that, and more, is what led us to create the view store as the central object that holds onto domain specific to a view. Both state and actions. A nice side effect of this also is that it comes with a performance boost if you do it correctly.
It is a very tiny step. We do want to eliminate We could be persuaded by |
I really don't know what you mean here. Assuming we could Getting back to the naming question, perhaps something that indicates the value is only for sending would be appropriate? Or maybe just create a type that can only |
When I saw the commit before spotting this discussion, I immediately thought that it should be named I personally think that the concept of I'm more reserved about the In other words, I don't know if the concept of That's a lot of words to say that I'm OK with the change in the context of a transitory solution to a problem we have today. |
Thanks @mbrandonw @tgrapperon for the insights. I'll just say that purely as a user, without deep insight into the complexities or transition plans. Something like this would be pretty convenient at times: store.snapshot { $0.users }
store.send(.addButtonTapped) |
"Snapshot" also came to mind but didn't think too much of it since I associate it with diffable data in UICollectionView. Seeing others bringing up "snapshot" makes me think there is something to it :) |
@jshier I think you misunderstood my statement. Suppose you had a view like this: struct ParentView: View {
let store: StoreOf<Parent>
var body: some View {
HStack {
ChildView(store: store.scope(state: \.child, action: Parent.Action.child))
Button("Tap") { self.store.send(.tap) }
}
}
} If you could send actions directly to So you might try restricting the actions that struct ParentView: View {
let store: Store<Parent.State, Parent.Action.View>
var body: some View {
HStack {
ChildView(store: store.scope(state: \.child, action: Parent.Action.child)) ❌
Button("Tap") { self.store.send(.tap) }
}
}
} …then you are out of luck. You can't possible scope the store to pass to the child because the store no longer holds onto any child actions. That was kind of its whole point too, to remove the non-view actions. So what you really need to do is hold onto two stores, one for the true domain of the feature (and it's child domains) and one for the view domain: struct ParentView: View {
let store: StoreOf<Parent>
let viewStore: Store<Parent.State, Parent.Action.View>
var body: some View {
HStack {
ChildView(store: store.scope(state: \.child, action: Parent.Action.child))
Button("Tap") { self.viewStore.send(.tap) }
}
}
} And so now we have re-invented the concept of view stores, but just in a weirder way. We have to hold onto two stores and nothing is stopping you from still sending non-view actions to the store. I want to stress that the situation is far more nuanced and delicate than just letting one send actions to the store. The view store is there for a reason (both for state and actions) and the alternative is not ergonomic. It really does serve as a scratch object just for the view where it is not appropriate to reuse the store, and that's the best way of thinking about it.
We went through lots of iterations for And we can't ignore the fact that there are an increasing number of people who are separating "view" actions from the rest of their actions. Really it is quite strange that you can technically send "effect" actions and child actions from a view when that is not intended at all.
@rcarver I understand the sentiment, but I think we need some concrete details of how this could actually work in practice and still serve the various use cases of the library. |
The idea of removing I'm not entirely sure what Observation brings to the table besides a uniform way of observing change. I'm sure it can help in some areas. |
A few months ago we moved to start making view store observations a lot more explicit. We now encourage people to do
WithViewStore(store: observe: { … })
andViewStore(store, observe: { … })
in order to chisel away at the state to the bare essentials the store needs.There's one big ergonomic problem with this that we never dealt with, and that's a quick way to send actions when you only have a
Store
. Technically you would have to do something like this:That's awkward, and gets even more awkward if the state is not equatable.
Really, view stores are just kind of awkward in general. We introduced them out of necessity but it's been a constant struggle to have good ergonomics. Luckily we think view stores aren't long for this world thanks to some of the new
Observation
stuff coming soon.So, we're looking to add some new API surface area to improve the view store ergonomics today, knowing that hopefully someday in the future we can completely remove the concept, or at least make it necessary only in a small number of situations.
We are suggesting adding a
withUnsafeStore
API that will give you instant access to the store's state and sending actions for a well defined scope:It's named "unsafe" because it does not perform any observation at all. You are only getting a snapshot of state at a specific time. And it's a little verbose, but a lot nicer than the alternative today.
What are people's thoughts on this?