So, you have written your first React application with Redux. You used redux-thunk, redux-promise or redux-saga as your middleware, enabling you to perform API calls with simple Redux actions. Life is great, but then you start to wonder, what exactly is that middleware doing with my actions? What kind of magic is going on when I write those thunks?
In this article, we will try to explain what happens there and how you can implement your own middleware for Redux, based on a popular option, redux-saga, which I highly recommend you check out.
A little background
If you aren’t familiar with Redux already, I will try to provide a very simplified explanation, without any actual syntax.
Redux is an application state container, which stores state in a single object called the store.
The store can get occupied only by the data that is returned by special functions called the reducers.
Reducers are pure functions, which means they always return the same result for a given input. That is the reason why Redux is called a predictable state container, you can always know what will be in the store based on the input received by the reducers. Those inputs received by the reducers are called actions. Actions always have a type and optionally carry additional data, based on which the reducers put data in the store. Then there’s the middleware, which sits just between the actions and the reducers. It is a mediator which can read the dispatched data (a fancy name for calling an action) and then do something with it. Usually, middleware is used for logging, sending error reports or fetching data asynchronously and then passing the action along to the reducer with the acquired data.
The workflow looks something like this.
If you used Redux before, chances are you’ve already used some middleware. Usually, you would use middleware that enables you to dispatch actions conditionally (or not to dispatch them), based on the result of some side-effect (an API call for example). But, middleware can actually be used for absolutely anything that you want to do with your data before it reaches the reducer, say logging, or sending an error report to the administrator if the application has crashed.
Creating the store
To keep this fairly short, I will use create-react-app to generate our application, with React already setup, and then install redux and react-redux to connect the two easily. We won’t be doing much React here, so don’t worry if you aren’t familiar.
The goal in our simple demonstration app will be to fetch data from some web API, save it to Redux with the use of our middleware and display it to the user.
Firstly, we will write a simple reducer, which will save the data received from the API. The API I will be using returns random person info, with the name, surname and country. This is the data which we want to save to the store. We will have three action types our reducer will handle: FETCH_DATA_REQUEST, FETCH_DATA_SUCCESS and FETCH_DATA_FAILED.
Our reducer would look something like this. We will put this bit of code in a new file called reducer.js.
Next, we want to create our store instance and put in a file called store.js. To create a Redux store we use the function createStore exported by the Redux package, which receives the root reducer with all the reducers combined through combineReducers and a store enhancer argument. We will use a built-in enhancer applyMiddleware, which will receive our middleware once we write it.
Our store.js would look like this.
Creating the middleware – implementing the base
Now, looking at the Redux docs (https://redux.js.org/advanced/middleware), we can see that middleware is a curried function that receives three arguments. If you don’t know what currying is, it’s basically returning a function from a function, each receiving a single parameter. The parameters are store, next (which is basically a dispatch function) and action.
If you take a peak at Redux docs, you can write your first middleware in seconds.
Congratulations, you have just written a logging middleware! This will log every action type to the console. But we don’t want to write a logging middleware right now, we want to make async calls with our middleware.
As I mentioned before, our middleware will be based on redux-saga. The basic principle in redux-saga is this, you set some watchers to watch for specific action types and execute a function which will handle that action, called the handler. So let’s start from there.
Since we want to save which actions we will be watching for, our middleware runner will have an actual instance, which will then determine if the middleware is executed or not.
We can write a simple class for that and put it in a new file called middleware.js. It will register action types that need to be processed and their handler function. The class can be called MySaga and looks something like this.
The method registerAction will save action type and handler function pairs to a Map, which provides us with a convenient way to access the handler function later on.
Creating the middleware – implementing the middleware runner
Now comes the tricky part.
Redux-saga is implemented with the use of generator functions. Generator functions unlike regular functions pause their execution when they encounter the yield keyword. Their instances also work like an iterator – you can call .next() method on them, which will return two things – an object with the value of the expression after the yield keyword, and a done property. When .next() is called, the generator function will resume its execution until it hits the next yield.
Finally, now comes the actual middleware part. The very middleware will be a method that can be called on a MySaga instance.
First, we want to check if the action that is currently in the middleware has a handler function.
We call next(action) at the end of the middleware so it can be processed by the next middleware in the chain (if it exists) and in the end, reach the reducer.
If the handler function (which is a generator) exists, we can call an instance of it and assign it to a variable and yield our first value. The goal is to somehow reach the end of the generator function by calling .next() until the done property is true.
I will now just paste the code below and explain what happens below.
First, we assign a generator function instance to a variable called handlerInstance and pass an action to it received by the middleware. At this moment, our handler function has already stopped at the first yield.
For our demo implementation we will just have two effects: put and call.
Call will have an asynchronous function that returns a Promise and an arbitrary number of arguments with which we want it to be called.
Put will have an action that you want to dispatch, it’s basically instructs the middleware call the dispatch function with the desired action.
We want to have some factory functions that yield those effects to the middleware. We can save them to a new file called effects.js.
You can now really see what happens in that while loop in the middleware. If the effect yielded is a “CALL“, we want to call that async function and wait for the result with the await keyword. As you can see, the while loop is wrapped in an IIFE (Immediately-invoked function expression), which allows us to use async/await in this block of code. When the Promise is resolved we can assign yieldedValue the next yield value and break out of the switch case. As you can see, we are calling.
next() method with the response data as its argument – that will evaluate the yield expression (with the yield keyword) in the generator function as this argument, making it possible to assign the data received from the Promise to a variable. If our Promise didn’t resolve, we can just throw an Error to our generator function with the .throw() method. If you aren’t familiar with .apply() method, it simply provides us a way to call a function with the arguments passed in as an array (in our case the array is the args property on the “CALL” efffect).
If the yielded effect is “PUT“ we just call the dispatch function and call the .next() method. The default case also calls the .next() method, so any yields that don’t return effects are ignored.
And that is about it, our middleware is complete. Now the only thing that’s left to do is to use it.
Using the middleware
To make use of our middleware we first have to make an instance of it and registering which actions we will be handling. We can do that in store.js so it looks something like this.
The fetchDataWorker is our handler generator function that we can write in a new file called sagas.js. Generator functions can be identified by the asterisk at the end of the function keyword.
Our file could look something like this.
I used axios to make a call to an API that returns random names and I put a little gender modifier just to test if our “CALL“ effect works properly when we pass it an argument. We wrapped the API call in a try/catch block for convenience. Remember, we throw the error to the generator function in the middleware, so that it can be caught here. So, in a nutshell, we first make an API call and when it is finished we store it into the response variable. This is possible because we called .next() with the response argument and only then can the generator function continue with the execution. After that, we simply dispatch a success action to be saved to the store. If the error occurs, we dispatch a “FETCH_DATA_FAILED” action.
Testing it in a React application
Now, we can finally test what we have written. We will delete everything that App.js returns in our src folder and create two buttons that fetch us a random female and male person. We also create a simple action creator to dispatch the “FETCH_DATA_REQUEST“ action. The App.js file looks something like this.
Add a little bit of CSS and voilà.
That’s our working async middleware in action! Of course, this is by no means a production ready solution, but it shows the basic principles for building a redux-saga like middleware.
Big respect to the folks that developed it.
The Saga continues
Congratulations for staying with me this far! I sincerely hope you learned something from this article and if not, I just hope that you had fun along the journey. It sure was a journey for me as well!
Now, the real adventure can begin. Have fun!