The Redux API provides for monitoring and adjusting the state store using a middleware API.
Middleware APIs can intercept requests to execute actions and generate side-effects. Common uses of Redux Middleware include:
- Persisting Redux state to local storage and restoring it on startup
- Logging actions and pre/post state for each dispatch
- Asynchronous processing in action creators via promise APIs
- Multi-step, long-running processes that watch the store and generate side-effects or other dispatches
- Storing forms data within redux
- Storing routing information within redux
- combining multiple reducers to firewall off state trees from each other
… and anything else you can think of.
Sample middleware projects with examples
Following we'll see how to configure various middleware and observe how they affect the Redux Store
#1: Redux Logger
If you don't have developer tools installed or want to monitor an application without setting breakpoints, you can install the Logger Middleware. Install with npm install ---save redux-logger
, then:
import { applyMiddleware, createStore } from 'redux'; import logger from 'redux-logger'; ... const store = createStore( reducer, applyMiddleware(logger));
Now, every request, starting and ending state will be logged in your developer console. For example:
You can also configure the logger with various settings, using the non-default import of createLogger
to construct it:
import { createLogger } from 'redux-logger'; const reduxLogger = createLogger({ diff: true }); ... const store = createStore( reducer, applyMiddleware(logger));
Here's a sample of the console output including a diff:
Another feature of the logger middleware: the predicate
property lets you filter out actions or state data you don't want to see.
It gets the state
and action
values of the incoming reducer, so you can decide whether to follow it to its resting state. For
example, in my game, I can filter just down to the reducer for a given player like this:
const reduxLogger = createLogger({ diff: true, predicate: (state, action) => { return action.type.startsWith('fooberry/player/'); } });
Pro tip: separate out your Redux code into multiple reducers with combineReducers
. This
function lets you pass an object with keys (the root name for each reducer in the Redux state) and values (references
to reducer functions). The state is then partitioned so that each reducer only manages a small part of the
overall state. See my reducer setup in the combineReducers function call in Fooberry, and then review the
various reducers in the sub-folders of the redux
directory for details.
One other thing: put this middleware last in your chain of middleware, or it won't log the results of certain other middleware like our next one, thunk
.
Check out the project homepage for more options. The state you save may be your own…
#2: Thunk Middleware
The redux-thunk middleware is extremely versatile. It allows developers to handle asynchronous processing and multi-step updates in their action creators.
Installed with npm install --save redux-thunk
, it then gets added with a simple middleware apply:
import thunk from 'redux-thunk'; ... const store = createStore(reducer, applyMiddleware(thunk, ...));
Once Thunk is installed, any action creator that returns a function delays evaluation of the function until runtime. That means that the function returned is dynamically evaluated and executed on every dispatch.
The function you return can have two parameters. The first is a reference to the store's dispatch
function. The second is a reference to a getState
function that can query the store's state at run time.
Armed with these facts, you can create action creators that fetch data, then use the fetched data to update a store when the state fetch is complete. An example from my sample game project, Fooberry:
export function loadNPCs() { return function(dispatch) { fetch('/npc-config.json') .then((data) => { return data.json(); }).then((data) => { dispatch({ type: LOAD_NPCS, payload: { data: data } }); }) .catch((e) => { console.log('failed npc load', e); }); }; }
In the example above, we use the HTTP fetch
library to load data from a JSON file, then we use a promise to resolve the fetch. This promise parses the data from JSON, then is chained to another then
to call the dispatch function, which will ultimately send the JSON data as a payload to store our non player character data.
Another example uses getState
to set up a multi-step action. Thunks can dispatch other actions simply by calling them inside of dispatch:
import Point from '../../Point'; import { move } from '../player/playerReducer'; import { logMovement } from '../logger/loggerReducer'; export function movePlayer(direction) { return (dispatch, getState) => { // make our move dispatch(move(direction)); // get data for logging position after move and // for checking for collisions const state = getState(); const playerName = state.player.name; const newPlayerPosition = state.player.point; const npcs = state.npcs; // log our move dispatch(logMovement( `player ${playerName}`, newPlayerPosition.x, newPlayerPosition.y) ); ... }
In the example above (taken from a complex action creator in Fooberry, when a player dispatches the movePlayer('east')
) action from a component, it executes a method that first makes the move, pulls the move data from the Redux state, then logs the move with another action.
#3: Redux Promise Middleware
Although the example above in #2 is fun, most things happen asynchronously, and so in many cases promises drive those interactions. Once you install the Redux Promise Middleware you can return a promise from any action creator. The payload
property of the action creator will then be a promise you create or return from an API.
The action type you use in your action creator does not get dispatched. Instead, it is used as a prefix, and the following actions are dispatched instead. Given a dispatched action type of MYACTION
:
MYACTION_PENDING
– dispatched immediately once the action creator is dispatched.MYACTION_FULFILLED
– dispatched if the promise resolves successfullyMYACTION_REJECTED
– dispatched if the promise is rejected
A simple action creator using the promise middleware:
export function getEmployees() { return { type: actions.GET_EMPLOYEES, payload: () => { return fetch('/api/employees') .then(response => { return response.json() } } }
In the example above, the fetch
API is used to load a set of employees from a web service. Once the employees are returned, the arrow function gets the response in a local .then
and returns a parsed payload. The Redux Promise Middleware
intercepts this call and dispatches the appropriate actions. The payload in the resolved action is the returned promise data:
... switch (action.type) { case MYACTION_FULFILLED: const employees = action.payload; return { ...state, employees: employees }; } ...
The payload for a rejected promise is the reason of the rejection.
In cases where you are chaining multiple then
requests together, you can use async
and await
together instead:
export function getEmployees() { return { type: actions.GET_EMPLOYEES, async payload: () => { const employees = await fetch('/api/employees').json(); const sales = await fetch('/api/sales').json(); return { employees: employees, sales: sales }; } }
Store Enhancers and Time Travel Debugging
You may hear another term bandied about with Redux: Store Enhancers. These are
functions that compose a new store out of one or more store creators.
The Redux DevTools
platform is one such store enhancer. The applyMiddleware
function is actually
a form of Store Enhancer.
The Redux DevTools enhancer enables you to travel time and step through actions
dispatched to your reducers.
There are actually two different approaches to using the developers tools: from
a Chrome plugin or via installation into the application itself.
The DevTools Extension: Easier…
Yes, turns out the DevTools extension is a far easier setup to enable. You
need to use the compose
function, plus one line of code that checks for the
Chrome Extension's additional JavaScript components:
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; ... // set up our composeEnhancers function, baed on the existence of the // DevTools extension when creating the store const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // now create your store as usual, but add the composeEnhancers function const store = createStore( combineReducers({ foo: FooReducer, bar: BarReducer, ... }, undefined, // or initial state provided by startup composeEnhancers( applyMiddleware( thunkMiddleware, promiseMiddleware, loggerMiddleware ) ) );
Given the above configuration, on a machine that already has the Redux DevTools
extension installed, it will be enabled when you switch to DevTools and switch
to that panel.
The Hard (but maybe more configurable) Way: Integrate Custom Inspectors
For the not-so-feint of heart, I give you the other mechanism: installing the
DevTools in your application directly, as a custom enhancer plus one or more
custom React components.
This project uses the redux-devtools
project as well as any number of other
component projects tied to it: redux-devtools-inspector
pretty much gives you
the main view of the normal Chrome DevTools, all wrapped up in a simple React
component that you can place anywhere in your application.
To set up the DevTools (I'll use the inspector component):
// in your store creating object import { createDevTools } from 'redux-devtools'; ... export const DevTools = createDevTools( <Inspector /> ); ... const store = createStore( combineReducers(...), undefined | initialState, compose( applyMiddleware(...), DevTools.instrument() // this installs the DevTools ) ); // in a component that displays your DevTools: import { DevTools } from './redux/my-store-creator'; export default class MainView extends Component { render() { ... return ( ... main view JSX here... <DevTools /> ); } }
Be forewarned: if you enable this tool, or even the compose enhancer for the Chrome
DevTools extension in production, you are giving your users the keys to the castle.
Then again, there is a movement afoot to let users access the Redux data in
a live production application
so go figure…
Wrap-up
So, that's it. Armed with the knowledge that you can add middleware to modify the
behavior of your Redux engine, and that middleware is simply one form of enhancer,
you can make the platform do all sorts of tricks, including handling async processes,
debugging, logging your state changes, and much more. Visit Awesome Redux
for a lot of great examples and a comprehensive list of middleware and enhancers.
The Plug – React and Redux Training
Since you've read this far, and hopefully you found the article useful, consider
attending our React and Friends training course.
Offered onsite at your location, for your employees, we take you from the basics
of building a React application through calling RESTful APIs, dealing with forms,
handling state in props/state and Redux, and learning how to use the React Router.