Redux: Is redux conflating actions with events?

Created on 13 Oct 2015  Â·  9Comments  Â·  Source: reduxjs/redux

I'm very new to redux, so bear with me.

I have some background in event sourcing, and the way it works is similar to redux, but not quite the same. In redux (or Flux in general), there is the concept of an _action_, that presuambly represents some intent either by the user or the system. There is also the concept of a _reducer_ that responds to those actions by applying each one to the current _state_ to generate a new _state_.

 state +---> +---------+            
             | reducer | +---> state
action +---> +---------+            

However, in event sourcing (ES for short), the concept of an _action_ is distinct from the concept of an _event_. The typical pattern in ES is that there is an _apply_ function that derives new state from an existing state and an event. So _apply_ is equivalent to a _reducer_ in redux. However, _apply_ relies on a series of events to derive new state, not actions. Actions, on the other hand, are executed by an _execute_ function, which takes the current state and an action, and produces zero or more events. (A good example can be found here, scroll to the Aggregate Module section.)

 state +---> +---------+              
             |  exec   | +---> [event]
action +---> +---------+              

 state +---> +---------+              
             |  apply  | +---> state  
 event +---> +---------+              

This separation between actions and events is fundamental to wrapping one's head around _intentions_ (actions) to change state, and _facts_ (events) that need to be reconciled into state due to the fact that those intentions were actually carried out.

By conflating the two together, it causes artifacts like creating async actions and async middlewares, while it would be much easier to always keep actions as plain objects, and create async _action handlers_ instead.

In addition, async actions/middlewares tend to produce other actions in response to the original action. The way I look at it, those async actions/middlewares should produce events, not actions, as a result of executing the original action. Those events will then be reconciled by the reducer in the typical manner. This is a much easier to understand pattern.

Middlewares are supposed to implement cross-cutting concerns, like logging or security, not application logic such as action handling.

Obviously I may be a bit biased towards ES, but I'd love to hear your opinion about what it has to offer.

Most helpful comment

My 2 cents follow.

Event Sourcing model:

 state +---> +---------+
             |  exec   | +---> [ event ]
action +---> +---------+

 state +---> +---------+
             |  apply  | +---> state  
 event +---> +---------+

Facebook Flux model:

                  state +---> +---------------+                   // - via SomeStore.getState()
                              | ActionCreator | +---> [ action ]  // - via Dispatcher.dispatch({})
ActionCreator arguments +---> +---------------+

                  state +---> +---------------+
                              |     Store     | +---> state  
                 action +---> +---------------+

Redux model:

                  state +---> +---------------+                   // - via thunk/getState()
                              | ActionCreator | +---> [ action ]  // - one via return {},
ActionCreator arguments +---> +---------------+                   //   many via thunk/dispatch({})

                  state +---> +---------------+              
                              |    Reducer    | +---> state  
                 action +---> +---------------+              

Personally, I see no significant difference except the terminology.

All 9 comments

We went with what's familiar to the target audience. The original target audience of Redux were Flux developers, so we considered it important to keep the Flux meaning of “action”.

This has been discussed before.

https://github.com/reduxjs/redux/issues/384
https://github.com/reduxjs/redux/issues/377
https://github.com/reduxjs/redux/issues/351

Thakns @gaearon. My apology for not going through the existing issues first.

I'm at least glad that I'm not the only one that sees the benefit of an alternative model, even though a decision has been made to keep things the way they are.

Isn’t the separation between action creators and actions in Flux/Redux more akin to the separation between actions and events in event sourcing?

@BurntCaramel Not quite. In CQRS / Event Sourcing, actions (more commonly known as commands) are intended for _action handlers_ (or command handlers), which know how to handle specific actions, and produce events as a result. Events are intended for persistence of state deltas, and for reconstructing state from a series of events.

In redux, there is no distinction between actions and events. They all go through the same path (the middleware pipeline), and eventually they hit the reducers. It's up to you to assign specific semantics to those actions. In some cases actions will be intended for thunk/promise middleware to perform async actions, and those never hit the reducers. Other times actions are pure objects, and eventually hit the reducers. And even in the latter case, redux doesn't prescribe whether those objects that hit the reducers are _commands_ or _events_. It's up to the application to make that distinction.

While this un-opinionated part of redux may be liberating for some people, I (and I guess some other people as well) find that it doesn't fit my mental model of having a clear distinction between commands that are intended for executing business logic, and events that are intended for persisting and reconstructing state.

Edit: Regarding action creators, those are not action handlers. They are intended for creating action objects, not handling those actions.

@khaledh I used to share many of your thoughts some time ago.
Also some things I posted initially to the DDD user group

I'm a backend guy that knows (mostly in theory) ES / CQRS / DDD, and I've implemented some of the concepts in our startup framework nearly 2 years ago (see details).

In the framework I build, there are commands and events. A command handler transform the command to events. There are also sagas.
It works fine until now, the app runs in production for quite a while, but actually for a long time i felt like i needed a lot of boilerplate for what i wanted to achieve.

The thing is that when we transpose these backend concepts to the frontend world, there are some adaptations to be done because the context is different.

In backend DDD / CQRS, the commands are actually representing the user intent. The command handler / AggregateRoot can choose to refuse to execute that command. When an application UX is well-designed, this generally happens when there's a concurrency issue: otherwise it would not be really good UX to display a button to the user for a forbidden action. The role of the aggregate root is to maintain consistency in a collaborative domain. Note that concurrency issues happen because of the async nature of network communications: the UI the user uses to emit its intent may eventually be stale.

In frontend / Flux, the UI is synchronous, and the user intents are sent "serially" to the app. There's no such thing as "collaboration" on the UI so there's no need to make any AggregateRoot to maintain consistency. The UI is synchronized to the UI state (with React) and when the user emit its intent, that intent is thus never stale (relative to the UI state, but it may relative to the backend state).
When an user clicks on a button on the UI, the intent may be refused by the backend, but it is never refused by the frontend, as the frontend still take action to try to serve the user intent. In any case, even if the command is rejected by the backend, you are still giving an error user feedback in your UI. This means that the UI state is modified, and so the command was accepted to produce error events.

The thing to understand is that the DDD domain you have on the backend is very different from the DDD domain you would use in the frontend.
Where I want to go is that after a while using commands and events in a 2 years old production React app, I have never encountered any usecase where a command is rejected. Even when the intent is refused by the backend, it generates some kind of event to provide user feedback.

So, for me the frontend, due to its synchronous nature, does not reject any command. Thus there is no need for AggregateRoot, and the command handlers always accept the commands and translate them to events (even for failure feedbacks, because it's still UI state modifications).

With that in mind, if you rename CommandHandler to ActionCreator, and Event to Action, you have Flux.

And a Saga could just be a Store/reducer that receive events and trigger an ActionCreator.

You might argue that sometimes the command handler needs to be stateful to decide which events to emit. I agree with that but actually in most cases you can provide that data to your React component as well, and fire the appropriate ActionCreator.

Again I'm still using commands and events and it works fine, but it really is a lot of boilerplate (maintaining commands, events and command handlers, where there seem to be a one-to-one relationship with all those...).

My 2 cents follow.

Event Sourcing model:

 state +---> +---------+
             |  exec   | +---> [ event ]
action +---> +---------+

 state +---> +---------+
             |  apply  | +---> state  
 event +---> +---------+

Facebook Flux model:

                  state +---> +---------------+                   // - via SomeStore.getState()
                              | ActionCreator | +---> [ action ]  // - via Dispatcher.dispatch({})
ActionCreator arguments +---> +---------------+

                  state +---> +---------------+
                              |     Store     | +---> state  
                 action +---> +---------------+

Redux model:

                  state +---> +---------------+                   // - via thunk/getState()
                              | ActionCreator | +---> [ action ]  // - one via return {},
ActionCreator arguments +---> +---------------+                   //   many via thunk/dispatch({})

                  state +---> +---------------+              
                              |    Reducer    | +---> state  
                 action +---> +---------------+              

Personally, I see no significant difference except the terminology.

@sompylasar That's a nice summary I think and is also aligned with how I see it.

I do think it is a little bit unfortunate that action has been settled in flux and redux, I understand why the choose it in redux to make it easier for the broader audience that understand flux to also understand redux. Using event instead of action would, in my mind, make much more sense. The reasoning is simple: an action is something you do and an event is something that has happened. In the redux scenario I just map my mind to use the terminology illustrated in previous comment.

@mastoj Yes, same thing. Please re-read and fix the words in your comment though to avoid even more confusion:

In the redux scenario I see the reducer as a action creators as action handlers, action as events and reducers as projection function create a view (the state).

@khaledh I think you make a lot of sense. I was thinking of just this. If we were using events rather than commands, we'd only dispatch event descriptions ("user-banned", "resource-requested", etc) and all the corresponding reducers that are interested in the action would react accordingly. Also, doing so would decouple the action from the reducer. Actions at that point would be global.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jimbolla picture jimbolla  Â·  3Comments

ilearnio picture ilearnio  Â·  3Comments

ms88privat picture ms88privat  Â·  3Comments

mickeyreiss-visor picture mickeyreiss-visor  Â·  3Comments

olalonde picture olalonde  Â·  3Comments