Redux Middleware and Enhancers: Getting Redux to log, debug and process async work

by
Tags: , , ,
Category:

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 successfully
  • MYACTION_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.