Is there any possible to send action to the store of sub view from the reducer of parent store?

Hi, I've struggled for this scenario for a while and still have not found the best practice yet.

For example, I have a view hierarchy A -> B, and each has its owns State/Action/Reducer.

Assume that you have an array [Model], and you can do operation to each Model at B view. A ForEachStore is used here apparently. The result of the operation comes from a delegate which is received at A in the form of a long Effect.

This means that ReducerA is able to mutate the state of the Model I operate at ViewB.

After the operation executed in ViewB and the value of the Model is modified in ReducerA (because only ReducerA handles the delegate Effect), I want to trigger another ActionB (executed in ReducerB) so that I can do some extra logic at ViewB.

The question is, how can I make it happen?

Now I have two workarounds:

  1. Move all these "extra logic" to ReducerA, which seems really bad because they should belong to ReducerB.

  2. Post a notification from ReducerA and ReducerB listens to this notification and execute those "extra logic"

It seems to me that both are not very good. Just wonder what's the best practice for this requirement. Is there any way for reducer to "push down" actions as opposed to Reducer.pullback ?


An example code to illustrate the question:

// MARK: Client of delegate
struct SomeDelegateClient {
    enum Action: Equatable {
        case update(id: UUID, state: Model.State)
    }

    var open: (AnyHashable) -> Effect<Action, Never>
}

// MARK: Parent State/Action/Environment/Reducer (A)
struct ParentState: Equatable {
    var children = IdentifiedArrayOf<Model>()
}

enum ParentAction: Equatable {
    case onAppear
    case delegate(SomeDelegateClient.Action)
}

struct ParentEnv {
    var open: (AnyHashable) -> Effect<SomeDelegateClient.Action, Never>
}

let parentReducer = Reducer<ParentState, ParentAction, ParentEnv> { state, action, env in
    struct DelegateID: Hashable {}

    switch action {
    case .onAppear:
        return env.open(DelegateID()).map(ParentAction.delegate)
    case let .delegate(.update(id, newState)):
        state.children[id: id]?.state = newState
        // [Question] How to send `ChildAction.didUpdate` to child store so that "extra logic" defined in childReducer can be executed.
        // Or any other solution? TCA allows us to mutate child state in the parent scope but how can we `push down` the action as opposed to Reducer.pullback ?
        return .none
    }
}

// MARK: Child State/Action/Environment/Reducer (B)
struct Model: Equatable, Identifiable {
    enum State: Equatable {
        case standby, completed
    }
    var state: State = .standby
    var id = UUID()
}

enum ChildAction: Equatable {
    case didUpdate
}

let childReducer = Reducer<Model, ChildAction, Void> { state, action, _ in
    switch action {
    case .didUpdate:
        // The "extra logic" to be executed
        return .none
    }
}

I believe you will need to combine your reducer A with a forEach reducer of Bs. For this you will need an A Action for the forEach casepath. You will then emit an Effect for this action which will run your ModelAction.

Your action for the Casepath would look something like case model(id: Int, action: ModelAction)

Your ParentAction lacks a case that wraps a ChildAction. Let's fix that first:

enum ParentAction: Equatable {
    case onAppear
    case delegate(SomeDelegateClient.Action)
    case child(id: UUID, action: ChildAction)
}

Now I can answer your question:

How to send ChildAction.didUpdate to child store so that "extra logic" defined in childReducer can be executed.

You don't need to send an action to a store. You need to apply a ChildAction to a Model.

You have an function that knows how to apply a ChildAction to a Model: childReducer. (A Reducer is just a function!) So one way to solve the problem is to call childReducer directly:

let parentReducer = Reducer<ParentState, ParentAction, ParentEnv> { state, action, env in
    struct DelegateID: Hashable {}

    switch action {
    case .onAppear:
        return env.open(DelegateID()).map(ParentAction.delegate)
    case let .delegate(.update(id, newState)):
        guard var child = state.children[id: id] else { return .none }
        let effect = childReducer.run(&child, .didUpdate, ())
        	.map { .child(id: id, action: $0) }
		state.children[id: id] = child
        return effect
    default:
        return .none
    }
}

Another way is to wrap the ChildAction.didUpdate in a ParentAction and then recursively call parentReducer. But the parentReducer you posted doesn't handle child actions. Use Reducer.combine and Reducer.forEach to handle child actions:

let parentReducer = Reducer<ParentState, ParentAction, ParentEnv>.combine(
    childReducer.forEach(
        state: \.children,
        action: /ParentAction.child(id:action:),
        environment: { _ in () }
    ),
    { state, action, env in
        struct DelegateID: Hashable {}

        switch action {
        case .onAppear:
            return env.open(DelegateID()).map(ParentAction.delegate)
        case let .delegate(.update(id, newState)):
            return parentReducer.run(&state, .child(id: id, action: .didUpdate), ())
        default:
            return .none
        }
    }
)
1 Like