Multiple optional reducers for the same Action type

I have a situation in which I have a Reducer which needs to needs to be combined with the same optional sub-reducer for two distinct optional states, only one of which is ever non-nil at any point. If I combine the sub-reducer twice, I get "Action sent to reducer where state is nil" errors in whichever of the two currently has the nil state.

I'll try to explain the scenario: I have a flow which can lead to a generic "form filling" flow, where the pages and fields of the form are driven by the backend. There are two states in the parent reducer that can lead to this form flow, only one of which can ever be non-nil at any given time:

struct FormState { 
    // state properties
}

enum FormAction {
    // form actions
}

struct FormEnvironment {}

let formReducer: Reducer<FormState, FormAction, FormEnvironment> = // form reducer

struct ParentState {
    var fooFormState: FormState?
    var barFormState: FormState?
}

enum ParentAction {
    case form(FormAction)
    // other cases
}

let parentReducer: Reducer<ParentState, ParentAction, ParentEnvironment> = 
    .combine(
        formReducer
        .optional()
        .pullback(
            state: \.fooFormState,
            action: /ParentAction.form,
            environment: const(FormEnvironment())
        ),
        formReducer
        .optional()
        .pullback(
            state: \.barFormState,
            action: /ParentAction.form,
            environment: const(FormEnvironment())
        )
    )

In the above example, if fooFormState is non-nil, and the form screen is showing, any action from the form will also be passed to the second instance of the formReducer for barFormState, which is nil, and therefore throws an assertion about actions being sent to nil states.

The only way so far I have been able to work around this is to combine fooFormState and barFormState into a single formState computed property and only combine the formReducer once:

enum FormType {
    case foo
    case bar        
}

struct FormState { 
    // state properties

    var type: FormType
}

enum FormAction {
    // form actions
}

struct FormEnvironment {}

let formReducer: Reducer<FormState, FormAction, FormEnvironment> = // form reducer

struct ParentState {
    var fooFormState: FormState?
    var barFormState: FormState?

    var formState: FormState? {
        get {
            if let state = fooFormState {
                return state 
            }

            return barFormState
        }

        set {
            guard let value = newValue else { return }
            switch value.type {
            case .foo:
                fooFormState = value
            case .bar:
                barFormState = value
            }
        }
    }
}

enum ParentAction {
    case form(FormAction)
    // other cases
}

let parentReducer: Reducer<ParentState, ParentAction, ParentEnvironment> = 
    .combine(
        formReducer
        .optional()
        .pullback(
            state: \.formState,
            action: /ParentAction.form,
            environment: const(FormEnvironment())
        )
    )

I was wondering if there was a better way to express this. I would ideally want to have something that expresses: only run the formReducer if Either barFormState or fooFormState is non-nil, and in particular, they should not both be non-nil simultaneously.

Not sure which framework you are using, looks like some version of Redux to me, but my knowledge on this is limited. However, when you write that you want some "Either" type that expresses that only A or B i there but not both, you can express this like so:

enum Either<A,B>{
case left(A)
case right(B)

var a : A? {
   if case .left(let a) = self{
      return a
   }
return nil
}

var b : B? {
   if case .right(let b) = self{
      return b
   }
return nil
}

}

You can obviously make a and b even mutable. Not sure how reducers are composed in the framework you use, but if one reducer knows how to manipulate a and the other knows how to manipulate b, it should be possible to make the necessary case distinctions to glue them together. In order to move from a to b, you would need a third reducer though.

Not sure which framework you are using, looks like some version of Redux to me

This sub-forum is specifically dedicated to the Swift Composable Architecture framework.

My problem was not so much being able to express the "Either"-ness (I ended up using an enum with two cases - a specific kind of Either) but rather with the action routing in TCA (The Composable Architecture).

The solution I found was simply to use two different action cases in the parent, each one mapping to the child action type:

struct FormState { 
    // state properties
}

enum FormAction {
    // form actions
}

struct FormEnvironment {}

let formReducer: Reducer<FormState, FormAction, FormEnvironment> = // form reducer

struct ParentState {
    var fooFormState: FormState?
    var barFormState: FormState?
}

enum ParentAction {
    case fooForm(FormAction)
    case barForm(FormAction)
    // other cases
}

let parentReducer: Reducer<ParentState, ParentAction, ParentEnvironment> = 
    .combine(
        formReducer
        .optional()
        .pullback(
            state: \.fooFormState,
            action: /ParentAction.fooForm,
            environment: const(FormEnvironment())
        ),
        formReducer
        .optional()
        .pullback(
            state: \.barFormState,
            action: /ParentAction.barForm,
            environment: const(FormEnvironment())
        )
    )

My bad, I was just clicking through the forum and thought "Swift Composable Architecture" was just a collective term.

1 Like