Redux — Command Bus or Event Store?
I’ve noticed an apparent inconsistency in the Redux community: the accepted practice seems to be to name actions in the present tense, as commands, but they are dispatched into an event store, which suggests an event sourcing pattern, in which case the actions should be named in the past tense. This mismatch may drive teams towards bad practices in choosing how to design actions and reducers. Specifically, the tendency is to use Redux as a pub-sub message bus where actions tightly couple the subscribers to the publishers, rather than using Redux as an event store where publishers describe what happened without needing to know what the subscribers will do.
Event Sourcing
Event Sourcing is a concept from Domain-Driven Design. Lots has been written about these topics, but in brief, under event sourcing a history of events is persisted in an event store, and the current app state is derived from reducers (pure functions that take the event history as input.) The app state derived from running the reducers on an event history is called the aggregate.
Multiple aggregates can be derived from the same event history. Each aggregate has its own reducer. This terminology is the same as from the concept of an aggregate function in SQL, and the reduce function in functional programming.
Typically the advantages of event sourcing are best realized for your application’s core domain (the primary problem that it is trying to solve) and for building a scalable CQRS architecture, so I was surprised to see it come up in a popular front-end technology. React + Redux users enjoy the event-sourcing pattern for the debugging facilities that it provides, such as the ability to inspect event history and rewind the app state.
Redux
Redux is a “predictable state container for JavaScript apps.” It provides methods to create event stores, dispatch events, and derive app state with reducers.
It has been claimed that the author of Redux, Dan Abramov, had not even heard of Domain-Driven Design when he created it. Nonetheless, the official Redux docs state that actions should be designed as events (emphasis theirs):
“Actions are plain JavaScript objects that have a type
field. As mentioned earlier, you can think of an action as an event that describes something that happened in the application."
They go on to provide a list of example events in past tense:
{type: 'todos/todoAdded', payload: todoText}
{type: 'todos/todoToggled', payload: todoId}
{type: 'todos/colorSelected, payload: {todoId, color}}
{type: 'todos/todoDeleted', payload: todoId}
{type: 'todos/allCompleted'}
{type: 'todos/completedCleared'}
{type: 'filters/statusFilterChanged', payload: filterValue}
{type: 'filters/colorFilterChanged', payload: {color, changeType}}
In spite of this (or perhaps because the docs have long since been updated), many of the Redux tutorials and examples name commands in present tense. The general understanding of how to use Redux is as a message bus with the event history and reducers being bells and whistles rather than the real point of the thing.
The Redux Anti-Pattern
Others have noticed this inconsistency as well. Yazan Alaboudi created a presentation entitled Our Redux Anti Pattern in which he talks about the trap of designing Redux actions that assume too much about the state that they are going to mutate, effectively turning Redux into a pub-sub message bus where the publishers are tightly coupled to the subscribers.
To riff on Mr. Alaboudi’s example, imagine a hockey game app where scoring a goal (an event) effects multiple state changes: the team’s score increments, the scorer’s season goals scored stats increments, the goalie’s goals allowed stat increments, the play is reset to a face-off at center ice, etc.
One way to implement this is to have an action for each of those state changes, and when a goal is scored each of those actions is dispatched:
addTeamGoals(scoringTeam, +1)
addPlayerGoals(scoringPlayer, +1)
addGoalieGoalsAllowed(goalieInNet, +1)
setGameState('faceoff')
- etc.
A better way to do it is just have one action capturing the event: goalScored(scoringTeam, scoringPlayer, goalieInNet)
and have separate reducers for each of the relevant state changes. This creates a separation between the event publishers and subscribers, such that the effects that events have can be updated without needing to touch the code that publishes them.
With reducers and events properly designed, if the rules of hockey are changed such that when Sidney Crosby scores a goal it counts as two goals instead of one, that logic could be implemented by only changing the relevant reducers. Similarly, if you wanted to add “longest time between goals” as part of your app state, you could implement that strictly in the reducers.
You can think of the actions as your app’s API, the reducers are back-end logic, and the store is a read model. That’s basically how I understand the CQRS pattern.
The Remedy
I’ve noticed the same anti-pattern playing out on the project I’ve been working on here at Tableau for the past year. The team I’m on started off designing some of our Redux actions with names like changeStage and setErrorType, where we used the current stage and/or current error to decide what React component to render.
One of our team members spotted this anti-pattern and remarked that we were using actions to directly control what component to render rather than deriving the fact of which component to render from the event history. This got me to thinking about Redux in general, and the more I learned about it the more I became convinced that we should be following event sourcing more broadly.
The app “stage” and “error” states are not things that should be set directly by dispatching an action with the desired end-state as parameters. The project in question is now moving toward actions as past-tense events with names like “settings button clicked” and “data sources request completed,” and the reducers are what determines the next app state. That way, when inspecting the event history in the Redux dev tools, it is not only apparent what state changes happened in what order, but also why those state changes happened.
Our team would have caught this architecture hiccup earlier if we’d known to name our actions in the past tense. Calling an action stageChanged feels wrong when it is the act of dispatching the action itself that changes the stage. One of our team members even pointed this out in a code review, when it was suggested to rename changeStage to stageChanged. The solution is not to rename the action, but to replace it altogether with other actions where the past tense name makes sense. Why did the stage change? Because a button was clicked.
Concluding Remarks
If you are implementing Redux actions, I strongly encourage following the convention of naming them in past tense and thinking of them as a chronicle of events stating what happened in the app. Use this practice to design actions that capture both what the state change was, and why it happened, when looking at the event history in the Redux DevTools.
Redux doesn’t have to be used as an event sourcing tool, although I argue that it is designed to be one. But when Redux is used as a simple pub-sub message bus, then the expressive power of writing meaningful events and reducers is lost.
For further reading, consider the following resources:
- Event Sourcing (Martin Fowler)
- Event Sourcing pattern (Microsoft Azure Architecture Patterns)
- Event Sourcing Made Simple (Kickstarter Engineering)
- Domain-Driven Design (Eric Evans, 2003, Addison-Wesley)
- Domain-Driven Design Distilled (Vaughn Vernon, 2016, Addison-Wesley)
関連ストーリー
Subscribe to our blog
Tableau の最新情報をメールでお知らせします