Hello, I have a struct FooClient with an enum FooClient.Action.
I have var fooClient: FooClient in my top level Environment and I pass it down to a few different view's focused environments as well. Those views have their own subtype struct State and enum Action, each of which as a case fooClient(FooClient.Action).
If I want to make a reducer that can access top level App.Stateand can react to FooClient.Action regardless of which view originates them, I have to do something like this:
switch action {
case .fooClient(let action),
.aView(.fooClient(let action)),
.otherView(.fooClient(let action)): // and so on
// update `App.State`
return .concatenate(
environment.barClient.doBarThing().map(App.Action.barClient),
environment.bazClient.doBazThing().map(App.Action.bazClient),
)
...
}
This works, but it's unfortunate I have to explicit list each place that can generate a FooClient.Action with a separate pattern match in the case.
If I don't do that I can combine the reducer multiple times, pulled back to each path so it's scoped directly to FooClient.Action, but then in order to be able to use barClient and bazClient I have to add case barClient(BarClient.Action) and case bazClient(BazClient.Action) to FooClient.Action so that that pulled-back reducer can use effects from those other clients.
The way Actions are currently structured as a hierarchy I don't see a way around this, but it would be nice if it was possible to respond to all FooClient.Action without having to enumerate them either inside the switch or inside the combine.
we haven't talked about this at all yet and don't have any documentation on it, and there are a few ways to approach this. One interesting way is to create a higher-order reducer that can allow sharing actions from one reducer to another.
For example, this:
extension Reducer {
func resending<Value>(
_ case: @escaping (Value) -> Action,
to embed: @escaping (Value) -> Action
) -> Self {
.combine(
self,
.init { state, action, _ in
if let value = CasePath.case(`case`).extract(from: action) {
return Effect(value: embed(value))
} else {
return .none
}
}
)
}
}
You can tack that onto an existing reducer to make any of its action be automatically re-sent to another reducer:
parentReducer
.resending(ParentAction.fooClient, to: { ParentAction.child1(.fooClient($0)) })
.resending(ParentAction.fooClient, to: { ParentAction.child2(.fooClient($0)) })
...
A workaround is to make resending take a case path instead of an embed, and then use case path composition:
(/ParentAction.child1).appending(path: /Action.fooClient)
// or
/ParentAction.child1 .. /Action.fooClient
If you want to discuss whether this breaking change was the right thing for us to do you could start a discussion on the TCA repo so that we could discuss more options and get more feedback from others.
Hi @mbrandonw I just refactored my code into a "resending" reducer, which is probably even better for performance, since I have many resending actions. So I'm fine with this braking change.