How can we enable the state transition of an application via asynchronous actions? How to enable, for example, state transitions involving a request to a Web server or the use of a timer? How do we integrate our application state with the data generated by an asynchronous action, while complying to the Redux architectural pattern?
Splitting the asynchronous action
The common approach to integrate asynchronous tasks into the Redux architecture is to break an asynchronous action into at least three synchronous actions.
An action informing that the asynchronous task:
- was successfully completed
Each of these actions changes the application state and keeps it in line with what is happening during the asynchronous task execution.
Implementing this approach requires that you dispatch the action that starts the asynchronous task. When the asynchronous task ends, a callback should manage the outcome of the asynchronous task and appropriately update the state with a positive or negative response.
That said, you may be tempted to support asynchronous actions by modifying their reducers, i.e. making sure that the reducer intercepting that action starts the asynchronous task and manages its outcome. However, this implementation violates the constraint that states a reducer to be a pure function. In fact, the result of an asynchronous task by its nature is based on a side effect.
So, let’s take a look at a couple of valid solutions to this problem.
Asynchronous actions and Thunk
A first approach is based on the Thunk middleware. The role of this middleware is very simple: verify if an action is a function and in which case execute it. This simple behaviour allows us to create actions no longer as simple objects, but as functions, which therefore have business logic.
So, in order to solve our problem with asynchronous tasks, we can define an action as a function that starts an asynchronous task and delegates its execution to the thunk middleware. Unlike the reducer, middleware is not required to be a pure function, so the thunk middleware can perform functions that trigger side effects without any problem.
As said before, you must first define three synchronous actions that represent changes in the state during the execution of the asynchronous task. Let’s define the following constants:
As you can see, they represent the three phases we described above.
Let’s now define an action creator for Thunk:
The first thing you can notice is that the action creator getQuoteAction() returns a function, as expected. The returned function starts dispatching the synchronous action QUOTE_REQUESTED and executes fetch() to actually start the asynchronous task, the HTTP request. Then, it dispatches one of the other two synchronous actions accordingly to the outcome of the asynchronous HTTP request.
Once we defined the transformation of an asynchronous action into three synchronous actions, we need to manage their impact on state transitions. So, let’s define the initial state of our application and the reducer that will manage quote retrieving:
The structure of the application state consists of a data array, containing the list of quotes to show (in our case we will have one quote), and a status string, representing the current status of the asynchronous action. The status property is not strictly required for the correct behaviour of the application, but it may be useful in order to give feedback to the user. The quotes() function implements a standard reducer by handling the three synchronous actions and generating the new application state accordingly.
The next step is to create the Redux store by specifying the use of the Thunk middleware, as shown by the following statement:
Finally, you have to manage the UI connecting it to the Redux store, as the following code shows:
As you can see, you dispatch the starting action by calling the getQuoteAction() creator and then subscribe to state changes. When a state change occurs, you check the status property value and inject the text inside the blockquote HTML element accordingly.
The final result in your browser will look like the following:
Try this code on CodePen.
Creating your own custom middleware
Redux Thunk elegantly solves the problem of managing asynchronous actions in Redux. However, it forces you to make the action creator’s code more complicated by sending the HTTP request and handling the response.
If your application heavily interacts with the server, as often it happens, you will have a lot of duplicate or quite similar code within the action creators. This distorts the original purpose of the action creators: creating an action based on parameters. Perhaps, in these cases, it is more appropriate to create an ad hoc middleware. The goal is to isolate the code that makes HTTP requests to the server in a special middleware and to restore the action creator to its original job.
So, let’s define a constant that identifies a meta-action for the HTTP request. We call it a meta-action because it is not the action that will directly modify the application state. Indeed, it is an action that will trigger an HTTP request and which will cause changes to the application state as a side effect by generating other actions.
The following is our constant definition:
Along with this constant, you need to define the constants that identify the actual action and the related synchronous actions to implement the HTTP requests, as we have seen before:
Now, you need the meta-action creator, that is an action creator that takes a plain action object as input and wraps it in order to create an asynchronous action to be handled via HTTP. The following is the meta-action creator that we are going to use:
You may notice that it returns an object with the HTTP_ACTION constant as its only property. The value of this property comes out from the action passed as a parameter combined with the action template. You notice that this template contains the general options for an HTTP request.
You will use this meta-action creator whenever you want to create an asynchronous action that will involve an HTTP request. For example, in order to apply this approach to retrieve the Ron Swanson random quotes described before, you can use the following action creator:
As you can see, any asynchronous action that involves an HTTP request can be defined by invoking the httpAction() meta-action creator with the minimal data to build up the request. You no longer need to add here the logic of synchronous actions generation, because it was moved into the custom middleware, as shown by the following code:
The middleware looks for the HTTP_ACTION identifier and replaces the current action with a brand new action with the _REQUESTED suffix. This new action is inserted in the middleware pipeline via next(). Then, it sends the HTTP request to the server and waits for a response or a failure. When one of these events occurs, the middleware generates the RECEIVED or FAILED actions as in the thunk-based approach.
At this point, the only thing you need to change to achieve the same result as in the thunk-based approach is the store creation:
You are saying Redux to create the store by applying your custom httpMiddleware instead of the Thunk middleware. The implementation of the reducer and the UI management remain as before.
You can try the implementation of this approach on CodePen.
In summary, we discovered that any asynchronous action can be split in at least three synchronous actions. We exploited this principle to implement two approaches for managing asynchronous actions while using Redux.
You may consider the first approach, based on the standard Thunk middleware, easier, but it forces you to alter the original nature of an action creator.
The second approach, based on a custom middleware, may seem more complex at a first glance, but it is much more scalable and maintainable.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.