"Action Splitting"

I once attended a talk about advanced Redux (another unidirectional architecture similar to SCA). It contained interesting and powerful ideas relating to taking advantage of the action layer. Patterns like "mapping", "splitting" and "enhancing" actions. This "higher order action" approaches were implemented at a middleware layer that doesn't exist in SCA but I imagine it could be replicated in the effects layer. Regardless of how implemented, it might be worth thinking about the utility of altering actions on their pathway to their final reducer and I'm curious about people's thoughts.

In practice, I've found a handful of use cases where this more dynamic treatment of how actions flow through an app might prove beneficial.

ex1)

Often I'll have (a) some top level modal state, and (b) a view that presents modally and dispatches a terminal action. Here, I can wed the terminal action to clearing the modal like this:

switch actionFromModal {
    case let .selectedEntity(_):
        state.modal = nil
}

but I can't help but notice we're disregarding the associated value.

also, lets say .selectedEntity is nested: .entities(.subEntity(.selected(id))). now we need to introduce a verbose pattern matcher, which i feel, knows to much about the reducer/action structure and is likely to change.

the benefit of action splitting here would be to send the more focused .clearModal action alongside .selectedEntity.

ex2)

Another example is selecting items after creating them. This is a common idiom. Without the capacity to do action splitting its handled implicitly in the reducer logic:

case .insertNew(entity):
    state.deselectAll()
    var entity = entity
    entity.isSelected = true
    state.append(entity)

Now lets say we have a .select(id: ID) action that corresponds to something like this:

case let .select(id):
    state.deselectAll()
    state.selectEntity(at: id)

We don't have a way of composing these at the action level. Because we don't have the ability to generate N actions from 1, we must create a new action .insertNewEntityAndSelect.

the benefit of action splitting here is clearly that you could dispatch .insert and .select together.

--

This seems a lot like treating actions as function calls rather than notifications that a side effect has occurred. If you want to reuse/compose behaviour why not use normal functions and call these from your reducer?

I agree there are arguments against this way of thinking. For example, it can be considered "imperative" to dispatch actions this way. But there are many powerful benefits to action splitting, enhancing, etc. You're gaining a lot of leverage by saying something like "every time a .sendAPIRequest action is dispatched, also dispatch a .setSpinner(true) action". sure, you can achieve that through plain functions in a reducer, but actions are what express the high level semantics of your app and its useful/powerful to program at that level as well.

From what I've seen of redux apps it is common for the reduce action handlers to contain all of the logic for responding to the action. In this case the actions do describe the semantics of your app. Another approach (which is far more common with Elm) is to model everything independently of the actions and then just call into this model from the action handlers. In this way you're able to achieve your example ("every time a .sendAPIRequest action is dispatched, also dispatch a .setSpinner(true)") within that model.

When .sendAPIRequest is matched by the reducer, just return an effect that wraps your .setSpinner action.

Effect(value: .setSpinner(true))

and to split an action you can use Effect.merge to wrap a list of effects together into a single effect, which runs the effects at the same time.

This works in most cases, but for stuff that are dependent on orders and use .concatenate, it doesn't work.

To illustrate this, we have actions A, B. Action A produces another action, C, which fetches stuff action B is dependent on. If you then run .concatenate(A, B) the list of actions would be [A, B, C] and not [A, C, B] which would solve our issue.