Redux is in part inspired by Flux, and the most common complaint about Flux is how it makes you write a lot of boilerplate. In this recipe, we will consider how Redux lets us choose how verbose we'd like our code to be, depending on personal style, team preferences, longer term maintainability, and so on.
Actions are plain objects describing what happened in the app, and serve as the sole way to describe an intention to mutate the data. It's important that actions being objects you have to dispatch is not boilerplate, but one of the fundamental design choices of Redux.
There are frameworks claiming to be similar to Flux, but without a concept of action objects. In terms of being predictable, this is a step backwards from Flux or Redux. If there are no serializable plain object actions, it is impossible to record and replay user sessions, or to implement hot reloading with time travel. If you'd rather modify data directly, you don't need Redux.
Actions look like this:
It is a common convention that actions have a constant type that helps reducers (or Stores in Flux) identify them. We recommend that you use strings and not Symbols for action types, because strings are serializable, and by using Symbols you make recording and replaying harder than it needs to be.
In Flux, it is traditionally thought that you would define every action type as a string constant:
Why is this beneficial? It is often claimed that constants are unnecessary, and for small projects, this might be correct. For larger projects, there are some benefits to defining action types as constants:
- It helps keep the naming consistent because all action types are gathered in a single place.
- Sometimes you want to see all existing actions before working on a new feature. It may be that the action you need was already added by somebody on the team, but you didn't know.
- The list of action types that were added, removed, and changed in a Pull Request helps everyone on the team keep track of scope and implementation of new features.
- If you make a typo when importing an action constant, you will get
undefined. Redux will immediately throw when dispatching such an action, and you'll find the mistake sooner.
It is up to you to choose the conventions for your project. You may start by using inline strings, and later transition to constants, and maybe later group them into a single file. Redux does not have any opinion here, so use your best judgment.
It is another common convention that, instead of creating action objects inline in the places where you dispatch the actions, you would create functions generating them.
For example, instead of calling
dispatch with an object literal:
You might write an action creator in a separate file, and import it into your component:
Action creators have often been criticized as boilerplate. Well, you don't have to write them! You can use object literals if you feel this better suits your project. There are, however, some benefits for writing action creators you should know about.
Let's say a designer comes back to us after reviewing our prototype, and tells us that we need to allow three todos maximum. We can enforce this by rewriting our action creator to a callback form with redux-thunk middleware and adding an early exit:
We just modified how the
addTodo action creator behaves, completely invisible to the calling code. We don't have to worry about looking at each place where todos are being added, to make sure they have this check. Action creators let you decouple additional logic around dispatching an action, from the actual components emitting those actions. It's very handy when the application is under heavy development, and the requirements change often.
Some frameworks like Flummox generate action type constants automatically from the action creator function definitions. The idea is that you don't need to both define
ADD_TODO constant and
addTodo() action creator. Under the hood, such solutions still generate action type constants, but they're created implicitly so it's a level of indirection and can cause confusion. We recommend creating your action type constants explicitly.
Writing simple action creators can be tiresome and often ends up generating redundant boilerplate code:
You can always write a function that generates an action creator:
There are also utility libraries to aid in generating action creators, such as redux-act and redux-actions. These can help reduce boilerplate code and enforce adherence to standards such as Flux Standard Action (FSA).
Middleware lets you inject custom logic that interprets every action object before it is dispatched. Async actions are the most common use case for middleware.
Without any middleware,
dispatch only accepts a plain object, so we have to perform AJAX calls inside our components:
However, this quickly gets repetitive because different components request data from the same API endpoints. Moreover, we want to reuse some of this logic (e.g., early exit when there is cached data available) from many components.
Middleware lets us write more expressive, potentially async action creators. It lets us dispatch something other than plain objects, and interprets the values. For example, middleware can “catch” dispatched Promises and turn them into a pair of request and success/failure actions.
The simplest example of middleware is redux-thunk. “Thunk” middleware lets you write action creators as “thunks”, that is, functions returning functions. This inverts the control: you will get
dispatch as an argument, so you can write an action creator that dispatches many times.
Thunk middleware is just one example of middleware. Middleware is not about “letting you dispatch functions”. It's about letting you dispatch anything that the particular middleware you use knows how to handle. Thunk middleware adds a specific behavior when you dispatch functions, but it really depends on the middleware you use.
Consider the code above rewritten with redux-thunk:
This is much less typing! If you'd like, you can still have “vanilla” action creators like
loadPostsSuccess which you'd use from a container
loadPosts action creator.
Finally, you can write your own middleware. Let's say you want to generalize the pattern above and describe your async action creators like this instead:
The middleware that interprets such actions could look like this:
After passing it once to
applyMiddleware(...middlewares), you can write all your API-calling action creators the same way:
Redux reduces the boilerplate of Flux stores considerably by describing the update logic as a function. A function is simpler than an object, and much simpler than a class.
Consider this Flux store:
With Redux, the same update logic can be described as a reducing function:
switch statement is not the real boilerplate. The real boilerplate of Flux is conceptual: the need to emit an update, the need to register the Store with a Dispatcher, the need for the Store to be an object (and the complications that arise when you want a universal app).
It's unfortunate that many still choose Flux framework based on whether it uses
switch statements in the documentation. If you don't like
switch, you can solve this with a single function, as we show below.
Let's write a function that lets us express reducers as an object mapping from action types to handlers. For example, if we want our
todos reducers to be defined like this:
We can write the following helper to accomplish this:
This wasn't difficult, was it? Redux doesn't provide such a helper function by default because there are many ways to write it. Maybe you want it to automatically convert plain JS objects to Immutable objects to hydrate the server state. Maybe you want to merge the returned state with the current state. There may be different approaches to a “catch all” handler. All of this depends on the conventions you choose for your team on a specific project.
The Redux reducer API is
(state, action) => newState, but how you create those reducers is up to you.