Composing Reducers #1297
Replies: 4 comments 11 replies
-
Hi @HarshilShah! Thanks for the detailed feedback! And glad to hear that your overall impression has been positive. There are a few tools shipping in the library that probably aren't highlighted enough in docs and migration guides that hopefully address some of these issues, at least partially.
I definitely agree! Generally it's good to group all core reducers together before layering on modifiers, especially since if you don't, you can run into the ordering problem The "ad hoc" way of doing this is using var body: some ReducerProtocol<State, Action> {
CombineReducers {
Scope(/* handle honest subfeature... */)
Reduce { /* reducer goes here */ }
}
.ifLet(/* handle optional subfeature... */)
} Another option is to extract the "core" composition into its own property, and delegate to that in the body, which layers on modifiers, though the explicit @ReducerBuilder<State, Action>
var core: some ReducerProtocol<State, Action> {
Scope(/* handle honest subfeature... */)
Reduce { /* reducer goes here */ }
}
var body: some ReducerProtocol<State, Action> {
self.core
.ifLet(/* handle optional subfeature... */)
} We're definitely open to improve these tools, but our current thought is that keeping Because of this, I think a With all that said, such a modifier is possible for you to define outside the library, so if you end up taking it for a spin in your applications, we'd love to hear how it works out for you and what ordering you decided on.
The core problem here is a routing problem, and more specifically, precise routing with an enum that nests all of its destination domains. Before diving into solutions, I hope we can agree that this is a pretty advanced topic and approach, and it's probably more typical to have a flatter domain with optional state/action pairs for each destination (this is also how folks have to model such things in vanilla SwiftUI). By avoiding the enum, you avoid the nesting, and you avoid the composition problem highlighted above: you would just chain some With all that said, we like precise modeling (even if it can be more cumbersome and verbose), and we built SwiftUI Navigation for that reason, so here's a way to model things precisely with a nested enum that should avoid the ordering issues you pointed out: var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
// Parent logic
}
.ifLet(\.route, action: /Action.route) {
Scope(state: /Routes.State.modal, action: /Routes.Action.modal) {
Modal()
}
// ...
}
}
struct Routes {
enum State {
case modal(Modal.State)
// ...
}
enum Action {
case modal(Modal.Action)
// ...
}
} I believe the Then, inside the You might see this version of If you want to tidy things up, you could even have var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
// Parent logic
}
.ifLet(\.route, action: /Action.route) {
Routes()
}
}
struct Routes: ReducerProtocol {
enum State {
case modal(Modal.State)
// ...
}
enum Action {
case modal(Modal.Action)
// ...
}
var body: some ReducerProtocol<State, Action> {
Scope(state: /Routes.State.modal, action: /Routes.Action.modal) {
Modal()
}
// ...
}
} One more thing: I agree that the argument for If var body: some ReducerProtocol<State, Action> {
Reduce { _, _ in
.none // List domain has no logic
}
.forEach(\.self, ...) { ... }
} Circling back to enum Sorry this got a lil long 😅 And I hope our current thought process makes sense. Feel free to press back on anything, and please do share more ideas if we can make these APIs better before release 😄 |
Beta Was this translation helpful? Give feedback.
-
Aside-reply to focus on this. Do you have a repro to work with? It could definitely be a compiler limitation, but it'd be nice to understand when it happens. Also, does switching to use enum |
Beta Was this translation helpful? Give feedback.
-
I'd say my view is the opposite of this. I find the chained modifier approach quite confusing, it behaves very differently from what it looks like and took a long time for me to understand:
This to me has several problems:
The Though I can't say I understand the ordering argument. Something about the parent potentially setting the state that the optional child reducer operates on to nil, before the child has a chance to work on it, so we should always run the child first. I may have misunderstood, but this doesn't seem like a problem to me. If the parent sets |
Beta Was this translation helpful? Give feedback.
-
I really appreciate the reply, but I can't really say I understand the reasoning!
Isn't that the whole point of As to the second point, I don't think it's quite correct. The two types of In a normal Looking closer, you could say that
Now it is indeed true that the Specifically, the modifier does two things:
I think you will agree that this is quite different in nature from the normal Personally, I find 1 very clean and easy to grasp, but 2 is a bit strange, even if it makes the code succinct and avoids an apparently very common pitfall. I also don't have a better suggestion that imposes the ordering. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Hiya folks. I've been updating some of my apps to use the
protocol-beta
and really loving it so far. This post has some feedback/wishes specific to composing the new ReducerProtocol types, which has been pretty much the only hairy bit of my migration so far.Of the 4 main APIs used to compose reducers,
Scope
is offered only as a standalone reducer, whileifLet
,ifCaseLet
, andforEach
are only offered as functions on ReducerProtocol, with the backing implementations all being internal to the repo.Here are some of the issues I’ve seen with this setup, and the use cases I have for them:
Scope
If you have a reducer that composes two reducers, one using optional state and the other non-optional, the body ends up looking like so:
It feels a bit strange to have any composed reducers declared at both extremities of the body. It would be great if there was an operator version of
Scope
available too so reducer bodies would be a bit easier to scan.IfCaseLet
A common pattern I’ve seen across some TCA projects I’ve worked on is having some feature live as part of an enum that is a property of the state of some larger feature. This is a common pattern for navigation, and TCA even includes SwitchStore specifically to handle this use case in SwiftUI, however there’s no corresponding ReducerProtocol API to make the reducers easier to glue together.
The best way to compose together these APIs right now is using
Scope
with anEmptyReducer()
and a bunch ofifCaseLets
s, as detailed here: 1This has the same scope issue of having to declare subfeatures in two places in the
body
, but theEmptyReducer()
also feels a bit crummy, and also not very obvious.If the
_IfCaseLetReducer
were public, however, the EmptyReducer() could be elided and instead a bunch ofIfCaseLetReducer
s could be used directly within theScope
, which combined with the aforementioned function style forScope
, would make a lot of code way more readable.I also think it might be useful to make public the backing reducers for
ifLet
andforEach
for similar use cases, but also I thinkifCaseLet
perhaps might have a stronger argument for a public API to allow for exactly this SwitchStore equivalent behaviourFootnotes
As an aside, I’m not quite sure if this is maybe a compiler limitation or some other bug, but in my testing I also wasn’t able to add more than 2
ifCaseLet
modifiers to theEmptyReducer()
; the compiler would just not be able to type-check the expression if I added a thirdifCaseLet
. I instead had to have 3Scopes
to accommodate all 5 cases of my state enum. ↩Beta Was this translation helpful? Give feedback.
All reactions