NavigationStackStore without Reducer Protocol

We have a big project that started with he first version of TCA and currently we're working with version 0.57.0

We're planning to migrate to the latest version of TCA but that will take some time to happen and we need to use NavigationStackStore but since all of our reducers are of type AnyReducer I can't find a way to implement it.

Found a way to use Reducer Protocol inside AnyReducer but couldn't find a way
to use AnyReducer in side Reducer Protocol
or mix them

Any suggestions?

We have a migration guide with a section "Embedding old reducer values in a new reducer conformance."

The main API available for converting an AnyReducer to a Reduce is by describing its environment in Reduce.init(_:environment:).

Thanks for you fast reply, is there a way to use Path's forEach in AnyReducer?

struct CounterDomain {
    struct Path: Reducer {
        ...
    }
    
    struct State: Equatable {
        var navigation: NavigationFeature.State = .init()
    }
    
    enum Action: Equatable {
        case navigation(NavigationFeature.Action)
    }
    
    static let reducer: AnyReducer<State, Action, Void>  = .combine(
        .init { state, action, _ in
            return .none
        }
        .forEach(state: \.path, action: /Action.path) {
            Path()
        }
    )
}

Tried to create an intermediary using Reducer Protocol and everything was working good until CaseLet appear CasePath didn't match the expected results

struct NavigationFeature: Reducer {
    struct Path: Reducer {
        ...
    }

    struct State: Equatable {
        var path = StackState<Path.State>()
    }

    enum Action: Equatable {
        case path(StackAction<Path.State, Path.Action>)
    }

    var body: some Reducer<State, Action> {
        Reduce { state, action in
            return .none
        }
        .forEach(\.path, action: /Action.path) {
          Path()
        }
    }
}

static let reducer: AnyReducer<State, Action, Void>  = .combine(
    AnyReducer { _ in
        NavigationFeature()
    }.pullback(
        state: \.navigation,
        action: Action.navigation,
        environment: ()
    ),
    .init { state, action, _ in
        return .none
    }
)

I think you need to wrap the AnyReducer in a Reduce. Something like:

    static let reducer: AnyReducer<State, Action, Void>  = .combine(
-       .init { state, action, _ in
-           return .none
-       }
+       Reduce(
+           .init { state, action, _ in
+               return .none
+           },
+           environment: …
+       )
        .forEach(state: \.path, action: /Action.path) {
            Path()
        }
    )

Version 0.57.0
So close, At this point

struct CounterDomain {
    struct Path: Reducer {
        ...
    }
    
    struct State: Equatable {
        var path = StackState<Path.State>()
    }
    
    enum Action: Equatable {
        case path(StackAction<Path.State, Path.Action>)
    }
    
    static let reducer: AnyReducer<State, Action, Void>  = .combine(
        Reduce(
            .init { state, action, _ in
                return .none
            },
            environment: ()
        )
        .forEach(\State.path, action: /Action.path) {
            Path()
        }
    )
}

I'm getting this error in the for each line

Cannot convert value of type '_StackReducer<Reduce<CounterDomain.State, CounterDomain.Action>, CounterDomain.Path>' 
to expected argument type '[AnyReducer<CounterDomain.State, CounterDomain.Action, Void>]'

I think you need to convert the Reducer back to an AnyReducer:

    static let reducer: AnyReducer<State, Action, Void> = .combine(
+     AnyReducer(
        Reduce(
            .init { state, action, _ in
                return .none
            },
            environment: ()
        )
        .forEach(\State.path, action: /Action.path) {
            Path()
        }
+     )
    )
1 Like

That certainly did the trick, thank you sir

1 Like

The sample code above does not contain any Environment but in the case it does noticed that now I have to initialize the environment 2 times in different places:

  • 1st time in the parents pullback
  • 2nd time inside the child reducer inside the Reduce init

is there any way to avoid creating the same environment 2 times?

When the environment contains a lot of properties creating 2 instances is a big issue

struct CounterDomain {
    struct State: Equatable {
        var tabBarState: TabBarDomain.State = .init()
    }
    
    enum Action: Equatable {
        case tabBar(TabBarDomain.Action)
    }
    
    static let reducer: AnyReducer<State, Action, Void>  = .combine(
        TabBarDomain.reducer.pullback(
            state: \.tabBarState,
            action: /CounterDomain.Action.tabBar,
            environment: { _ in
                return TabBarDomain.Environment(sampleText: "Hello Counter")
            }
        ),
        .init { state, action, _ in
            return .none
        }
    )
    
}
struct TabBarDomain {
    struct Path: Reducer { ... }
    struct State: Equatable { ... }
    enum Action: Equatable { ... }
   
    struct Environment {
        let sampleText: String
    }
    
    static let reducer: AnyReducer<State, Action, Environment> = .combine(
        AnyReducer(
            Reduce(
                .init { state, action, environment in
                    ...
                },
                environment: TabBarDomain.Environment(sampleText: "Hello TabBar")
            )
            .forEach(\.path, action: /Action.path) {
                Path()
            }
        )
    )
}

Creating an environment value shouldn't be a big deal, and is ideally just temporary as you migrate away from AnyReducer you will be able to eliminate the environment entirely.

1 Like