A Beginner's Guide to Redux Middleware
Redux has become the state container of choice for React apps. The key idea that makes redux so popular is that your application logic lives in "reducers", which are JavaScript functions that take in a state and an action, and return a new state. Reducers are pure functions: they don't rely on or modify any global state, so they're easy to test, reason about, and refactor. For example, here's a redux store that keeps track of a counter:
const redux = require('redux');
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
}
return state;
};
const store = redux.createStore(counter);
Once you have a redux store, you can subscribe to state changes in the store and dispatch actions:
store.subscribe(() => { console.log(store.getState()); });
store.dispatch({ type: 'INCREMENT' }); // Prints "1"
Reducers are an elegant tool for managing state, but they're not the full story when it comes to building applications with redux. If you look carefully, you'll notice 2 implicit constraints on what redux reducers can do:
- Reducers must be synchronous. They
return
the new state. - Because reducers should not modify global state, reducers should not use functions like
setInterval()
Let's suppose you wanted to use redux as a state container for a stopwatch application: the application should be able to display the elapsed time on the screen and then save the elapsed time to a server using an HTTP request. Reducers are great for transforming state due to synchronous actions like button presses, but what happens when you need to throw some asynchronous behavior into the mix? That's where the idea of middleware comes in.
Introducing Middleware
Your stopwatch application needs the ability to display the amount of time elapsed. The right way to do this is for your reducer to listen for 3 actions:
START_TIMER
, fired when the timer startsTICK
, fired when you should change the current amount of time elapsedSTOP_TIMER
, fired when you're not going to receive any moreTICK
actions.
Here's how the reducer looks:
const stopwatch = (state = {}, action) => {
switch (action.type) {
case 'START_TIMER':
return Object.assign({}, state, { startTime: action.currentTime, elapsed: 0 });
case 'TICK':
return Object.assign({}, state, { elapsed: action.currentTime - state.startTime });
case 'STOP_TIMER':
return state;
}
return state;
};
const store = redux.createStore(stopwatch);
The above function is just a plain old reducer: it doesn't rely on or modify global state, and it's fully synchronous. When the user clicks a button to start the timer, you can dispatch the START_TIMER
event, and when they click a button to stop the timer, you dispatch STOP_TIMER
.
However, there's a problem: you need to periodically dispatch TICK
events to update the elapsed time. You can call setInterval()
in the START_TIMER
case statement, but then your reducer modifies global state and you violate redux best practices. The right place to periodically dispatch TICK
events is in middleware:
const timerMiddleware = store => next => action => {
if (action.type === 'START_TIMER') {
action.interval = setInterval(() => store.dispatch({ type: 'TICK', currentTime: Date.now() }), 1000);
} else if (action.type === 'STOP_TIMER') {
clearInterval(action.interval);
}
next(action);
};
const stopwatch = (state = {}, action) => {
switch (action.type) {
case 'START_TIMER':
return Object.assign({}, state, {
startTime: action.currentTime,
elapsed: 0,
interval: action.interval
});
case 'TICK':
return Object.assign({}, state, { elapsed: action.currentTime - state.startTime });
case 'STOP_TIMER':
return Object.assign({}, state, { interval: null });
}
return state;
};
const middleware = redux.applyMiddleware(timerMiddleware);
const store = redux.createStore(stopwatch, middleware);
The redux middleware syntax is a mouthful: a middleware function is a function that returns a function that returns a function. The first function takes the store
as a parameter, the second takes a next
function as a parameter, and the third takes the action
dispatched as a parameter. The store
and action
parameters are the current redux store and the action dispatched, respectively. The real magic is the next()
function. The next()
function is what you call to say "this middleware is done executing, pass this action to the next middleware". In other words, middleware can be asynchronous.
The timerMiddleware
function above is responsible for managing the setInterval()
function, including clearing the interval when the STOP_TIMER
action is dispatched. Redux calls the timerMiddleware
function when a new action is dispatched, before the reducer. This means the middleware can transform actions as necessary, including dispatching new actions. When you run the above code in Node.js, you should see approximately the below output.
$ node
> const store = require('./test.js');
undefined
> store.subscribe(() => console.log(store.getState().elapsed));
[Function: unsubscribe]
> store.dispatch({ type: 'START_TIMER', currentTime: Date.now() });
0
undefined
> 1002
2005
3013
4015
5017
6019
7021
store.dispatch({ type: 'STOP_TIMER' })
7021
undefined
>
Great, you now have a working timer in redux! The TICK
events get fired roughly once a second, so you'll get periodic state changes with the elapsed time.
Resolving Promises With Middleware
You now have a working stopwatch, but your app still needs to be able to save the time to the server. How are you going to handle HTTP requests in redux? The key is the next()
function and promises:
const timerMiddleware = store => next => action => {
if (action.type === 'START_TIMER') {
action.interval = setInterval(() => store.dispatch({ type: 'TICK', currentTime: Date.now() }), 1000);
} else if (action.type === 'STOP_TIMER') {
clearInterval(action.interval);
}
// next() passes an action to the next middleware, or to the reducer if
// there's no next middleware
next(action);
};
Remember that middleware can be asynchronous. The next()
function is middleware's flow control mechanism: it's how you defer control to the next middleware in the chain. You can call next()
asynchronously, or even not at all. You can create a middleware that resolves promises for you. If you dispatch an action with a payload
property that's a promise, the below middleware will wait for that promise to resolve or reject before calling next()
.
const promiseMiddleware = store => next => action => {
// check if the `payload` property is a promise, and, if so, wait for it to resolve
if (action.payload && typeof action.payload.then === 'function') {
action.payload.then(
res => { action.payload = res; next(action); },
err => { action.error = err; next(action); });
} else {
// no-op if the `payload` property is not a promise
next(action);
}
};
You can add this middleware to your redux store using the createMiddleware()
function.
// Order of execution is timerMiddleware first, promiseMiddleware second
const middleware = redux.applyMiddleware(timerMiddleware, promiseMiddleware);
const store = redux.createStore(stopwatch, middleware);
How does this help with HTTP requests in redux? HTTP clients like superagent return promise-compatible interfaces, so if you set an action's payload to a superagent request, the promiseMiddleware
will wait for the request to complete before passing the action along.
const superagent = require('superagent');
store.dispatch({ type: 'SAVE_TIME', payload: superagent.post('/save', store.getState()) });
The above dispatch()
call will save the elapsed time to the server. All your reducer needs to do is handle the result.
const stopwatch = (state = {}, action) => {
switch (action.type) {
case 'START_TIMER':
return Object.assign({}, state, {
startTime: action.currentTime,
elapsed: 0,
interval: action.interval
});
case 'TICK':
return Object.assign({}, state, { elapsed: action.currentTime - state.startTime });
case 'STOP_TIMER':
return Object.assign({}, state, { interval: null });
case 'SAVE_TIME':
// If there was an error, set the error property on the state
if (action.error) {
return Object.assign({}, state, { error: action.error });
}
// Otherwise, clear all the timer state
return Object.assign({}, state, { startTime: null, elapsed: null, error: null });
}
return state;
};
In the redux paradigm, your reducer should be responsible for any modifications to the state. Your middleware should not modify the state. However, your middleware should be responsible for any interactions that affect global state (like setInterval()
) or any asynchronous operations (like HTTP requests).
Other Middleware Applications
While resolving promises is the most common use case for middleware, there are numerous other use cases for middleware.
- Logging:
const loggerMiddleware = store => next => action => {
console.log(action.type);
next(action);
}
- Showing a toast message when there's an error using vanillatoasts:
const toastMiddleware = store => next => action => {
if (action.error) {
vanillatoasts.create({ text: action.error.toString(), timeout: 5000 });
}
next(action);
};
- Waiting for the user to confirm:
const confirmationMiddleware = store => next => action => {
if (action.shouldConfirm) {
if (confirm('Are you sure?')) {
next(action);
}
} else {
next(action);
}
};
Next Steps
Middleware is essential for building any non-trivial redux application. Any asynchronous behavior or global state modifications should go through middleware, so your reducers can be pure functions. If you're interested in learning more about how to use middleware to build real-world redux applications, check out this sample application, which is a medium clone written in React with Redux as the state container. This app is the basis for an upcoming video course on Thinkster about Redux, so sign up on GitHub for updates!
This is not beginners guide at all
please give me orther way: require(’./test.js’);
Very well written, thanks! I was wondering if there is a way to connect my custom middleware to the state. So I could check if
isUser
for example in order to navigate to login page if not authenticated. Is there a way to do that?You can use
store.getState
inside of your middlewares.