How to Add Reducers for Non-optional Route?

I have a parent feature that contains a non-optional route property:

struct LocationAuthorization: ReducerProtocol {
    struct State: Equatable {
        enum Route: Equatable {
            case locationAuthorizationDenied(LocationAuthorizationDenied.State)
            case locationAuthorizationPrompt(LocationAuthorizationPrompt.State)
            case locationAuthorizationRestricted(LocationAuthorizationRestricted.State)
        }

        var route: Route
        //... other properties for parent feature
    }

    enum Action: Equatable {
        enum Route: Equatable {
            case locationAuthorizationDenied(LocationAuthorizationDenied.Action)
            case locationAuthorizationPrompt(LocationAuthorizationPrompt.Action)
            case locationAuthorizationRestricted(LocationAuthorizationRestricted.Action)
        }

        //... other actions to be handled by parent feature
        case onAppear
        case route(Route)
    }

    var body: some ReducerProtocol<State, Action> {
        Reduce { state, action in
            switch action {
            case .onAppear:
                //... logic to determine which route to show. default to view for explaining why location authorization is needed and CTA to initiate location authorization prompt.
                state.route = .locationAuthorizationPrompt(.init())
                return .none
            case .route:
                return .none
            }
        }
    }
}

Each of the routes maintain some of their own state, such as views to explain that location authorization is denied, restricted, or needed. Some of them can send the user to the device Settings app, while others contain other relevant location-based logic, which is why they are their own cases in a non-optional enum.

When running the app, the view for prompting the user for location authorization is displayed, as expected. However, the user is not prompted when the CTA is tapped because the LocationAuthorizationPrompt reducer is not initialized in the parent reducer body.

When using an optional route, the ifLet operator can be used on the reducer, but I want to use a non-optional property because there should never be a scenario in which one of the routes is not shown.

Can you help me understand what I need to add to the parent reducer so that the reducers for the routes are able to run?

var body: some ReducerProtocol<State, Action> {
    Reduce { state, action in
        switch action {
        case .onAppear:
            //... logic to determine which route to show. default to view for explaining why location authorization is needed and CTA to initiate location authorization prompt.
            state.route = .locationAuthorizationPrompt(.init())
            return .none
        case .route:
            return .none
        }
    }

    // What goes here?
}

This is where the semantics of TCA are not perfect right now. So you have to jump through a couple of hoops here...

In my project I have a similar "appMode" state that switches the app between logged out, logged in, maintenance mode, etc... So it also isn't optional.

To translate to your case it would be like this...

var body: some ReducerProtocol<State, Action> {
	Scope(state: \.route, action: /Action.route) {
		EmptyReducer()
			.ifCaseLet(/State.Route.locationAuthorizationDenied, action: /Action.Route. locationAuthorizationDenied) {
				DeniedReducer()
			}
			.ifCaseLet(/State.Route.locationAuthorizationPrompt, action: /Action.Route.locationAuthorizationPrompt) {
				PromptReducer()
			}
			.ifCaseLet(/State.Route.locationAuthorizationRestricted, action: /Action.Route.locationAuthorizationRestricted) {
				RestrictedReducer()
			}
	}

	Reduce { state, action in ... }
}

Note it has to come before the main reducer. This is because it is possible for the main reducer to change the value of route which would mean if the "route reducer" comes after it it would no longer have the correct state for the action it is trying to process and it will error.

1 Like

Brilliant! I was doing a couple of things incorrectly, which you clarified.

Firstly, I was attempting to place the reducers after the main reducer. Your explanation helped me to better understand the logic for placing if above the main reducer.

Secondly, I was not using an EmptyReducer, so I was struggling to figure out how to extract the possible route cases in order to use the reducers inside of the Scope.

Thank you for your assistance!

No worries. You don't NEED to use EmptyReducer() you can use any Reducer of the Route state.

So you could have...

Reduce { state, action in }
  .ifCaseLet...

But... if you don't need to actually do anything in the Reducer then might as well use the EmptyReducer(). :smiley: