Menu hamburger icon

Time Travel in React Redux apps using the Redux DevTools

Time Travel with the Redux DevTools

In the previous article we showed how to create a simple drawing application with React and manage the state of the app with a Redux store. Now we will talk about how to integrate the Redux DevTools to enable easy debugging and time travel.

The Redux DevTools records dispatched actions and the state of the Redux store at every point in time. This makes it possible to inspect the state and travel back in time to a previous application state without reloading the page or restarting the app.

There are two ways to integrate the Redux DevTools with your app. The DevTools are actually a React component so you can embed it in your React app. Another way to use them is by installing the Chrome extension. We will take a look at both options in this post.

Using the Redux DevTools in conjunction with React Hot Loader makes for very powerful debugging. We have already discussed hot reloading in a previous article.

If you are not yet familiar with Redux, please take a look at Patrick’s great article where he builds a calculator app using Redux and vanilla JavaScript.

In the next few days we will release another article which describes how to create a more complex app using React with Redux and the Onsen UI React Components. The example is a weather forecast application. The code for that application can be found here. You can also try out the demo.

The Redux DevTools

In the app below the Redux DevTools are embedded on the right side. The app is a very simple grid where you can toggle the cells by clicking them. Please feel free to play around with it.

How to time travel

The DevTools make time travel debugging very easy. When you fire upp the app you can see the initial state under the @@INIT header:

Initial state of the app

As we can see the state object tree has three keys: width, height and cells. The width and height parameters are the dimensions of the grid and cells is an array of 1s and 0s that describe the current image. The initial state is a smiley face.

If you click on one of the cells the TOGGLE_CELL action will be dispatched and the state will be updated. If you try toggling one of the pixels you will see something like this in the DevTools:

Redux DevTools after an action has been dispatched

The DevTools display the dispatched action:

{
  type: 'TOGGLE_CELL',
  x: 3,
  y: 6
}

By recording all the dispatched actions we can easily create automated tests and it also makes it easy to reproduce bugs since the tester can record all the actions leading up to the bug and send it to the developer.

To go back to the previous state you can cancel the TOGGLE_CELL action. This is done by clicking on the action in the DevTools.

Redux DevTools after an action has been canceled

We have now returned to the previous state which happens to be the initial state in this case. If you dispatch several actions you don’t need to cancel the actions from the bottom and up, it’s actually possible to cancel an action in the middle of the list. Redux will rebuild the state given the new list of actions.

The animation below shows the time travel in action:

Redux DevTools time travel animation

Tutorial: Embedding the Redux DevTools

Now we will take a look at how to embed the Redux DevTools in an application. As mentioned earlier the DevTools is a React component.

The DevTools is very modular and there are several different monitors available to debug and visualize the Redux store state. In this example we will use the following monitors:

  • DockMonitor - A resizable and moveable dock.
  • LogMonitor - A log of states and actions which allows changing the history to enable time travel.

Take a look at this page for a list of other available monitors. It is of course also possible to create custom monitors to suit your own needs.

So before we get started we need to install some packages. I assume that you already have a React project with a Redux store. Otherwise, please take a look at this article.

npm install --save-dev redux-devtools redux-devtools-{log,dock}-monitor

To create the DevTools component the createDevTools function is used. We put the component in a different file so we can import it only if it’s needed. This is important because you probably don’t want the DevTools running in a production environment.

DevTools container

import React from 'react';
import {createDevTools} from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';

/**
 * Create the DevTools component and export it.
 */
export default createDevTools(
  <DockMonitor
    /**
     * Hide or show the dock with "ctrl-h".
     */
    toggleVisibilityKey='ctrl-h'
    /**
     * Change the position of the dock with "ctrl-q".
     */
    changePositionKey='ctrl-q'
    defaultIsVisible={true}
  >
    <LogMonitor theme='tomorrow' />
  </DockMonitor>
);

I put this file in ./containers/DevTools.js so it can be imported from the main program.

DevTools store enhancer

To hook up the DevTools component with your Redux store you need to use the DevTools.instrument() function that returns a store enhancer. A store enhancer can be passed to the createStore function to, as the name implies, enhance the Redux store.

import {createStore} from 'redux';
import DevTools from './containers/DevTool';
import rootReducer from './reducers';

const initialState = {
    ...
};

const store = createStore(
    rootReducer,
    initialState,
    DevTools.instrument()
);

A lot of times you also want to use some middleware or other enhancer at the same time as the DevTools component. In that case you can use the compose() function from the core Redux library.

import {createStore, compose} from 'redux';

...

const store = createStore(
    rootReducer,
    initialState,
    compose(
        applyMiddleware(logger),
        DevTools.instrument()
    )
);

Main program

import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {createStore, applyMiddleware, compose} from 'redux';
import createLogger from 'redux-logger';

import rootReducer from './reducers';
import App from './components/App';

/**
 * Import the DevTools container.
 */
import DevTools from './containers/DevTools';

const logger = createLogger();
const initialState = {};

/**
 * This variable is "true" if the application
 * is running in production.
 */
const isProduction = process.env.NODE_ENV === 'production';

let store;

if (isProduction) {
  store = createStore(rootReducer, initialState);
}
else {
  /**
   * Only use the DevTools component
   * when in development.
   */
  const enhancer = compose(
    applyMiddleware(logger),
    DevTools.instrument()
  );

  store = createStore(rootReducer, initialState, enhancer);
}

const rootElement = document.getElementById('root');

render(
  <Provider store={store}>
    <div>
      <App />
      /**
       * Embed the DevTools component only
       * when in development.
       */
      {!isProduction && <DevTools />}
    </div>
  </Provider>,
  rootElement
);

In the code above we use the process.env.NODE_ENV environment variable to determine whether the application is running in production or not. This variable is not available in the browser so it needs to be defined in the Webpack config. To do that we use the webpack.DefinePlugin function to create a custom plugin that defines this variable when running in production. To do that we put the following lines in the webpack.config.prod.js file:

plugins: [
  new webpack.DefinePlugin({
    'process.env': {
      'NODE_ENV': JSON.stringify('production')
    }
  })
]

Tutorial: Using the Redux DevTools Chrome Extension

If you want to get started quickly with the Redux DevTools, another option is to use the Redux DevTools Chrome Extension. By installing this you will get a new tab labelled “Redux” in the Chrome DevTools as you can see in the image below.

Redux DevTools Chrome Extension

However, this is not enough to start debugging Redux driven applications. Just like in the case where we use the React component directly we need to add an enhancer to the createStore() call. Thankfully the Chrome Extension exposes a variable called window.devToolsExtension when the extension is installed so all we have to do is pass that to the createStore() function:

const store = createStore(
  rootReducer,
  initialState,
  compose(
    applyMiddleware(logger),
    /**
     * Conditionally add the Redux DevTools extension enhancer
     * if it is installed.
     */
    window.devToolsExtension ? window.devToolsExtension() : f => f
  )
);

If you have the Redux DevTools Chrome Extension installed you can try it out with the Weather App Demo since that app will use the DevTools if they are installed.

Apart from being very easy to use, another great thing about the Chrome Extension is that it doesn’t require React. If you are writing Angular 2 apps with Redux to store your state, using the Chrome Extension is a lot easier than embedding the React component since that would add another dependency to the app.

Conclusion

In this article we have seen how easy it is to install the Redux DevTools to enable powerful debugging and time travel in Redux driven applications. In an upcoming article we will take a look at how to create a more complex app with Redux and React with the Onsen UI React components. Please stay tuned!

Redux is definitely the most popular option for state management in React lately. However, there are lots of other options available. Patrick will show how to use MobX in his next article.

If you haven’t already, we recommend you to check out the Onsen UI React Components. It provides a large collection of UI components for creating hybrid mobile apps. We love getting stars on GitHub!

Comments