Recursive reducer on optional sub-states

I have a state and action that looks like this:

struct MyState: Equatable {
      var a: Int
      @Indirect var b: MyState?
      @Indirect var c: MyState?
}

indirect enum MyAction: Equatable {
      case b(MyAction)
      case c(MyAction)
      case someRootAction
}

My first attempt was to implement a reducer like this:

let myReducer: Reducer<MyState, MyAction, Void> = .combine( 
     myReducer.optional().pullback( 
           state: \.b,
           action: /MyAction.b,
           environment: ()
     ),
     myReducer.optional().pullback( 
           state: \.c,
           action: /MyAction.c,
           environment: ()
     ),
     Reducer<MyState, MyAction, Void> { state, action, _ in 
           switch action {
           case .someRootAction:
                   print("Root action")
                   return .none
           }
     }
)

However, that is making the library to crash at Reducer.swift line 451 even though state is not null:

return self.reducer(&state!, action, environment)

How can I model this type of recursive state/reducers?

Check out this recursive higher-order reducer from the SwiftUI Case Studies on the TCA's GitHub Page. Hope this helps!

Another solution could be:


protocol ReducerProtocol {
   associatedtype State
   associatedtype Action
   associatedtype Environment
   func apply(to state: inout State,
              action: Action,
              environment: Environment) -> Effect<Action, Never> 
}

extension ReducerProtocol {
   func asReducer() -> Reducer<State, Action, Environment> {...}
}

extension Optional {
   func modify<T>(default defaultValue: T,
               closure: (inout Wrapped) -> T) {
      guard var value = self else {
          return defaultValue
     }
     self = nil //prevent copy on write 
     closure(&value)
     self = value 
   }
}

Just conform to the ReducerProtocol, and you get a named function apply that you can reference inside the function body:

struct MyReducer {

   func apply(to state: inout MyState,
              action: MyAction,
              environment: ()) -> Effect<MyAction, Never> {
         switch action {
            case .b(let b):
               state.b.modify(default: .none) {apply(to: &$0,
                                                     action: b,
                                                     environment: ()}
            case .c(let c):
               state.c.modify(default: .none) {apply(to: &$0,
                                               action: c,
                                               environment: ()}
            case .someRootAction:
                //whatever
         }
   }

}

Suggestion: you could reduce some boilerplate, if your enum looked like this:

indirect enum MyAction {
   case someRootAction
   case recursive(action: MyAction, keyPath: WritableKeyPath<MyState, MyState?>)
}

Great suggestions guys! I ended up doing it the way it's done in the case study, but I will give a try to @Anachron's since the reducer looks cleaner in my opinion.

Terms of Service

Privacy Policy

Cookie Policy