Filter actions in scoped store

Hey :wave:,
I am quite new to TCA and started playing around with it.
I am currently not sure about the correct technique to filter actions from a subview so the parent won't receive the actual root action when scoping the store.

Here is a small code example that tries to show the problem:

IfLetStore(
    store.scope(
        state: \.xyInputViewState,
        action: { localAction in
            switch localAction {
                case .began:
                    return .noteOn
                case .ended:
                    return .noteOff
                default:
                    return .none // <- NOT POSSIBLE TO RETURN .NONE
            }
        }
    ),
    then: XYInputView.init(store:)
)

The only solutions I came up with is to create my own .none action and just do nothing in the reducer.
Is this the intended way to do such things?

Typically the XYInputView view would have its own domain that the parent domain holds onto.

So, for example, if you had a child domain like this:

struct ChildState {...}
enum ChildAction {...}
let childReducer = ...

Then the parent would embed the child domain into its domain:

struct ParentState {
  var child: ChildState?
  ...
}
enum ParentAction {
  case child(ChildAction)
  ...
}
let parentReducer = ...

And then you could use IfLetStore like so:

IfLetStore(
  store.scope(state: \.child, action: ParentAction.child),
  then: ChildView.init(store:)
)

Hey @mbrandonw ,
maybe I wasn't clear enough about what I want to achieve but your solution is bascially what I did in the first place. But In this case the parent-reducer receives all possible child actions. I want the parent to receive a mapped version of what the childrens are doing. The Idea behind this, is that I have a wrapper view that can hold multiple child views with different actions and I don't want the parent of this view to receive the specific action of every possible subview that could be displayed by this view. If I would follow your guidance I would need to add multiple child states to the ParentState and multiple childActions to the ParentAction.

If you follow this code example you maybe understand what my issue is.
I am currently not sure if this somehow is against the main idea of TCA.
But for me it feels wrong that every tiny action of every view will be received by the root parent.

The code should compile ;). Thx in advance for your help.

//MARK: Parent
struct ParentState: Equatable {
    var wrapperViewState: WrapperViewState
}
enum ParentAction {
    case wrapper(WrapperViewAction)
}

struct ParentEnvironment {}

struct ParentView: View {
    let store: Store<ParentState, ParentAction>
    var body: some View {
        WithViewStore(self.store) { viewStore in
            WrapperView(store: self.store.scope(state: \.wrapperViewState,
                                                action: ParentAction.wrapper))
        }
    }
}

let parentReducer: Reducer<ParentState, ParentAction, ParentEnvironment> = .combine (
    parentBaseReducer,
    wrapperReducer.pullback(
        state: \.wrapperViewState,
        action: /ParentAction.wrapper,
        environment: { _ in .init() }
    )
)
let parentBaseReducer = Reducer<ParentState, ParentAction, ParentEnvironment> { state, action, env in
    switch action {
        case let .wrapper(childAction):
            // The Parent knows everything about the specifics of the child views actions
            // My goal is to combine A and B and put an abstraction above childViewActionA and childViewActionB
            switch childAction {
                case .childViewActionA:
                    return .none
                case .childViewActionB:
                    return .none
            }
            // The Parent should not know anything about the implementation of the Child Views actions
            // It should receive a filtered and adapted version of what happened inside the Wrapper.
            // Something like this:
            //        case let .wrapper(childAction):
            //            switch childAction {
            //                case .mappedAction:
            //                    return .none
            //            }
    }
}

//MARK: Wrapper
struct WrapperViewState: Equatable {
    var childStateA: AChildViewState
    var childStateB: BChildViewState
}
enum WrapperViewAction {
    case childViewActionA(AChildViewAction)
    case childViewActionB(BChildViewAction)
}

struct WrapperViewEnvironment {}

struct WrapperView: View {
    let store: Store<WrapperViewState, WrapperViewAction>
    var body: some View {
        VStack {
            AChildView(store: store.scope(state: \.childStateA,
                                         action: WrapperViewAction.childViewActionA))
            BChildView(store: store.scope(state: \.childStateB,
                                         action: WrapperViewAction.childViewActionB))
        }
        
    }
}

let wrapperReducer: Reducer<WrapperViewState, WrapperViewAction, WrapperViewEnvironment> = .combine (
    achildReducer.pullback(
        state: \.childStateA,
        action: /WrapperViewAction.childViewActionA,
        environment: { _ in .init() }
    ),
    bchildReducer.pullback(
        state: \.childStateB,
        action: /WrapperViewAction.childViewActionB,
        environment: { _ in .init() }
    ),
    wrapperBaseReducer
)

let wrapperBaseReducer = Reducer<WrapperViewState, WrapperViewAction, WrapperViewEnvironment> { state, action, env in
    switch action {
        case .childViewActionA:
            return .none
        case .childViewActionB:
            return .none
    }
}

//MARK: Childs A & B
struct AChildViewState: Equatable {}
enum AChildViewAction {
    case aAction
}

struct AChildViewEnvironment {}

struct AChildView: View {
    let store: Store<AChildViewState, AChildViewAction>
    var body: some View {
        WithViewStore(self.store) { viewStore in
            Button(action: { viewStore.send(.aAction)}, label: { Text("Send action A") })
        }
    }
}

let achildReducer = Reducer<AChildViewState, AChildViewAction, AChildViewEnvironment> { state, action, env in
    switch action {
        case .aAction:
            return .none
    }
}

struct BChildViewState: Equatable {}
enum BChildViewAction {
    case bAction
}

struct BChildViewEnvironment {}

struct BChildView: View {
    let store: Store<BChildViewState, BChildViewAction>
    var body: some View {
        WithViewStore(self.store) { viewStore in
            Button(action: {
                viewStore.send(.bAction)
            }, label: { Text("Send action B") })
        }
    }
}

let bchildReducer = Reducer<BChildViewState, BChildViewAction, BChildViewEnvironment> { state, action, env in
    switch action {
        case .bAction:
            return .none
    }
}

In order for a child store to be able to process a child action, it must be sent through the parent store for it to reach the app-level reducer in the first place, which is inevitably responsible for invoking the child reducer with that action internally.

While you can't prevent a parent from observing that child actions are being sent, you can use access control to "hide" the contents of certain child state and actions:

public enum ChildAction {
  case actionVisibleFromParent
  case anotherActionVisibleFromParent
  case internal(Internal)

  public struct Internal {
    internal var action: Action

    internal enum Action {
      case anInternalAction
      case anotherInternalAction
    }
  }
}

The caveat is if you want to test these actions, they must be internal and not private, and if they are internal you must modularize your feature in order to prevent the parent domain from accessing them. There is also quite a bit of boilerplate, but maybe it could be eliminated with access control for enum cases.

We've generally found that this kind of hiding goes against the grain in TCA, though. Do you have a concrete, motivating use of such a pattern? Why shouldn't a parent know what their child is up to? :wink:

If a child feature truly needs local domain that the parent shouldn't know about or observe at all, that domain needs to live outside of the reducer (e.g. as some local @State and action closures, or in a local TCA store that communicates with the child store).

1 Like

Hey Stephen, thanks for your feedback.
Maybe the overall architecture I set up is still not the best ;).

I try to give you more background information about
what I want to achieve and also a graph to go even deeper.

My idea was to provide a container-view (as shown in the example -> wrapper)
that is able to show different Input methods for a sound generator.
This could be for example a keyboard, a XY-Pad, Device-Motion etc.
Each of these inputs is displayed via a custom view to the user.

I thought it would be a nice Idea to put the sound generation into the parent view so the sound generation is managed globally and to put a container in the middle that receives all the different input values from the input views -> for example the XY-Pad will return an x and a y value, the motion input a velocity and rotation etc. The wrapper then forwards a mapped version of the children's inputs to the parent.

If I would implement it like you proposed, the parent will receive all the redundant information from all the different input methods and needs to map everything by itself. Wouldn't it be better if the mapping happens before the action reaches the parent?