.forEach reducer and non-exclusive ElementState

Hi,

Let's say that I have such a simple reducer:

struct OneOfManySelection: ReducerProtocol {
    struct State {
        let ints: [Int]
        var selected: Int?
    }

    enum Action {
        case select(id: Int)
    }

    var body: some ReducerProtocol<State, Action> {
        ...
    }
}

The logic, that I'm after is simple - whenever the select action gets sent I want to set the given id as selected. I'm tempted to use a forEach here and model the ElementState as:

struct SingleSelectionState {
    let int: Int
    var selected: Int?
}

This is problematic though. In order to wire it up I'd have to come up with a computed property, here's a possible solution:

var _ints: IdentifiedArray<Int, SingleSelection.State> {
    get {
        IdentifiedArray(
            uniqueElements: ints.map {
                SingleSelectionState(int: $0, selected: selected)
            },
            id: \.int
        )
    }
    set {
        selected = newValue.first(where: { $0.selected != selected })?.int
    }
}

The get part is not so bad, but the set is a bit nasty. It works, but meh... Doesn't feel right.

Are there any other options available to use forEach and leverage its niceness without having to go with such a computed property? Or is forEach not the right tool at all for a situation where some non-exclusive ElementState (the selected part) is involved?

1 Like

Hi @Czajnikowski !

Do you have any reason why not using the action select within the reducer to set the action associated value to the state.selected property? I mean, like:

...
var body: some ReducerProtocol<State, Action> {
        Reduce { state, action in
            switch action {
                case let .select(value):
                    state.selected: value
                    return .none
            }
        }
    }
...

Thanks for the answer @otondin

My example is simplified to the essence (shared selected and exclusive int). In real life, you can expect a more complex reducer/state with way more exclusive logic/state and dependencies, that can benefit from the isolated nature of each Element reducer/state. So yeah, I can do it with an inline Reduce as you proposed, but I'm looking for the "best practice" to follow when it comes to forEach in general.

Without forEach you have to always use some id explicitly in the calling code (viewStore.send(.select(int))), instead of viewStore.send(.select)) which is always a bit more error-prone. The calling code is usually outside of the easily testable logic (UI), so the concern gets more important. Also - you don't always land in a situation where your ElementState is non-exclusive up front. You may start with the exclusive one and forEach and then, when you figure out that you need some shared state as well, you may have to face some non-super-trivial refactoring if you don't want to end up with not-so-nice wiring logic (_ints.set as an example).

2 Likes