Pullback on a stateless store

Hello guys, I'm struggling to find the way to pullback from a stateless store.

Here's some code to explain the issue

I've a ChildView that has a stateless store

let childReducer = Reducer<Void, ChildAction, ChildEnvironment> { state, action, environment in
    switch action {
        
    case .restorePurchasesTapped:
        return environment.storeClient.sync()
            .fireAndForget()
        
    case .resetTapped:
        return .none
        
    case .unlockTapped:
        return .none
    }
}

On the ParentView I need to pullback the childReducer, but the thing is that the pullback asks me for a WritableKeyPath<GlobalState, Void> but I've no idea how to provide one

let appReducer: Reducer<AppState, AppAction, AppEnvironment> =
    .combine(
        trainingPlanSettingsReducer.pullback(
            state: <#T##WritableKeyPath<GlobalState, Void>#>,
            action: /AppAction.trainingPlanAction,
            environment: { appEnv in
                ChildEnv()
            }),
        Reducer<TrainingPlanState, TrainingPlanAction, TrainingPlanEnvironment> { state, action, environment in
            switch action {
            case .startStage(stage: let stage, stageIndex: let index):
                return .none
         }
}

It’s not very pretty, but you could add a Writable Void property (computed or not) to the parent AppState, that way you can reference its keypath.

Since there's only one value (“inhabitant”) of type Void, a pullback for a Reducer where State == Void can just make up a Void value to pass to the wrapped Reducer, and discard the value after the wrapped Reducer returns, without needing a WritableKeyPath. Here's a copy of pullback specialized for State == Void:

extension Reducer {
  func pullback<GlobalState, GlobalAction, GlobalEnvironment>(
    action toLocalAction: CasePath<GlobalAction, Action>,
    environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment
  ) -> Reducer<GlobalState, GlobalAction, GlobalEnvironment>
  where State == Void
  {
    .init { _, globalAction, globalEnvironment in
      guard let localAction = toLocalAction.extract(from: globalAction) else { return .none }
      var state: Void = ()
      return self.reducer(
        &state,
        localAction,
        toLocalEnvironment(globalEnvironment)
      )
      .map(toLocalAction.embed)
    }
  }
}

You should be able to use it like this:

let appReducer: Reducer<AppState, AppAction, AppEnvironment> =
    .combine(
        // Assuming trainingPlanSettingsReducer's State == Void
        trainingPlanSettingsReducer.pullback(
            // No state keypath needed
            action: /AppAction.trainingPlanAction,
            environment: { appEnv in
                ChildEnv()
            }),
        Reducer<TrainingPlanState, TrainingPlanAction, TrainingPlanEnvironment> { state, action, environment in
            switch action {
            case .startStage(stage: let stage, stageIndex: let index):
                return .none
         }
}
2 Likes

That makes sense, thanks a lot @mayoff I'll give that a go!