Pullback reducer on enum property (SwitchStore)

Once again, I am coming back to you guys with a question regarding the composable architecture, since I always get extremely helpful answers on here.
I am experimenting with the SwitchStore but not directly on a state, but a enum property of a state.
My State and Action look something like this:

enum EnumState {
  case a(StateA)
  case b(StateB)
}
struct RootState {
  var enumState: EnumState
  ...
}
enum RootAction {
  case subActionA(ActionA)
  case subActionB(ActionB)
  ...
}

In the view I can very simply use a SwitchStore by scoping the store to enumState:

SwitchStore(store.scope(state: \.enumState)) {
  CaseLet(state: /EnumState.a, action: Action.subActionA) { storeA in
    ...
  }
  ...
}

Where I am stuck is how to pullback the reducers operating on StateA/ActionA and StateB/ActionB to my root reducer.
What I tried and what was working is adding a computed property var stateA: StateA? to my root state and than pulling back an optional reducer, but I don't like this approach because the code for the optional property is not pretty and can (in theory) reach unhandleable states.

What I think should work, is pulling the reducer back twice, first with a case path to operate on EnumState, and then on the RootState but I was not able to produce code which compiled.
Any help is appreciated!

If it would help, I could create an example project and upload it to Github.

4 Likes

A double pullback was indeed the right solution, I just had to do a little bit of try and error to find the right syntax. The trick was to use a Case path for Action which used self.

var intermediateReducer: Reducer<EnumState, RootAction, RootEnvironment> {
  return reducerA.pullback(
    state: /EnumState.a,
    action: /RootAction.subAcrtionA,
    environment: { .init(from: $0) })
}

var reducer: Reducer<RootState, RootAction, RootEnvironment> {
  .combine(
    intermediateReducer.pullback(
      state: \.enumState,
      action: /RootAction.self,
      environment: { $0 }),
    .init { state, action, environment in
      ...
    })
}
3 Likes

Hey @GrafHubertus, glad you figured it out! That is indeed the best solution for right now. You could even chain the two .pullbacks one after the other if you didn't want to create the intermediate reducer, and you can even shorten the identity case path with /.self :laughing:

FWIW, we do have plans to make this a bit nicer in the future. While key paths are great for structs, and case paths are great for enums, there's still a 3rd concept that comes up when you try to compose a key path with a case path (as you are seeing here). We are still working on the ergonomics of this concept, but once it's ready it means you will be able to accomplish what you want with a single pullback.

6 Likes

I ran into the exact same problem some months ago and had come up with a convenience pullback that combines a case path with a writable keypath. Maybe that's something that might come in handy until the work on ValuePaths is finished. :)

https://github.com/pointfreeco/swift-composable-architecture/pull/653/files

2 Likes

Same problem, solved using the double pullback above.. Very grateful to have found this thread lol

1 Like

How would this scenario be handled if enumStates were an array?

    var enumStates: [EnumState]

I can do the root level forEach reducer, but for the intermediate reducer, I can't seem to figure out what the state value would be, as enumStates is an array.

intermediateReducer.pullback(
      state: \.enumStates, ??? \.enumStates[index] ???
      action: /RootAction.self,
      environment: { $0 }),

Any suggestions would be appreciated. Thanks.