Improve your development workflow with Redux DevTools Extension


TL;DR: I just released Redux DevTools Extension v2.7, which brings “pausing” and “locking” features to improve the way of developing and debugging Redux apps. As a consequence, extension’s store enhancer is being deprecated.

Hot Reloading with Time Travel helps to boost the developer’s productivity significantly and makes the development fun. It could be more powerful visual alternative to TDD. However, you might not taking advantage of its full potential:

I never got to implementing the real workflow I imagined, so “Revert / Commit” buttons at the top only hint at it in a non-friendly way.
Here’s how I imagined it. Say I’m working on a feature that involves complex local state transformations in response to user actions.
In the last app I was working on, there was a drag-and-drop post editor with nested entities, handling of async responses, etc.
Say I want to add a new type of post content. I added a button that dispatches an action but there is no handler so it’s a no-op now.
I press “Record” and perform a few actions. Some of them may already be handled by reducers, some may not. Then I press “Stop”.
Now DevTools enters a “loop” mode. It ignores any other actions in the app. I can’t interact with UI in my app. I’m focused on one scenario.
Now I work on the reducer code. When I save a file, DevTools replays my “scenario” and shows computed states. I can switch between steps.
I know that no stray AJAX request will mess up my state, that everything is completely predetermined by the scenario (actions) I recorded.
I keep editing my reducers and see how that affects the computed states of the whole scenario. Runtime errors are displayed inline.
Most likely, I will mess up some existing handlers while adding new feature — since scenario includes other actions, I will notice and fix it.
Finally, the whole scenario is correct. Every computed state looks good, and UI looks good with every state.
Now I can reorder actions right in DevTools to make sure that I don’t have weird race conditions. If I have any, I can keep fixing reducers.
Finally when I’m ready, I exit the “loop mode”. I can work on the next feature or tweak the UI. And I can “save” the scenario to unit tests.

As Dan Abramov stated, some pieces are missing here. I’m implementing them in Redux DevTools Extension v2.7, though not exactly as described above. Basically, the “loop mode” is split into “pausing” and “locking”.

Pausing the recording of dispatched actions

In a real-world app usually there are tons of actions dispatched, and you can easily get lost in the recorded history. For such cases Redux DevTools becomes pretty useless. The solution would be to have a Pause button:

When the button is toggled, Redux DevTools instrumentation will not record the other actions, so you can work with just the current history, but still recomputing the future states:

While pausing, you can still time travel, skip and recompute actions. Also you can add new actions explicitly, just click Dispatcher, and dispatch even action creators (specified in actionCreators option):

If you click Commit (before or after pausing), only the current state will be stored. So it will also solve the memory issues, caused by storing large action’s payloads and states, especially when profiling the performance.

You can easily export the recorded history for a specific feature or generate tests for it.

Locking non-explicit changes

Usually it’s not enough just to pause the recording, we also want to forbid dispatching of other new actions. So we could focus on a specific feature we’re developing and, whenever we change something in the code, hot-reloading will recompute only the actions before locking without any additional requests. Just click the Lock button:

Unlike pausing, here we’re freezing the app to be sure that nothing will mess up the state (unless we’re skipping actions or making any changes in our reducers):


The other benefit of locking is that we finally have a solution to avoid side effects (we could even auto lock changes while time travelling).

However, to implement that, dropping all future actions isn’t enough. We need to prevent side effects from:

  1. subscribed functions and connected React components;
  2. middlewares and store enhancers.

Let’s take the following example:

store.subscribe(() => {
if (store.getState().subscribedServerToModel) {
sendToServer(store.getState().substate);
}
}

Whenever you dispatch an action, the state is sent to the server. But you don’t want that while time travelling.

A better approach would be to move all side effects to middlewares, which the extension would lock automatically when needed. See Redux real-world example for that:

function sendToServerMiddleware(store) {
return next => action => {
const result = next(action);
if (store.getState().subscribedServerToModel) {
sendToServer(store.getState().substate);
}
return result;
};
}

Or use vendor middlewares. For example for Redux Thunk our example becomes:

function sendToServerAction() {
return (dispatch, getState) => {
sendToServer(getState().substate);
dispatch({ type: 'FETCH_SUCCEEDED' });
// You might also dispatch 'FETCH_FAILED'
};
}
store.subscribe(() => {
if (store.getState().subscribedServerToModel) {
store.dispatch(sendToServerAction());
}
}

Using the current extension API we cannot prevent the middlewares and enhancers calls, as Redux DevTools instrumentation and extension’s enhancer are at the end of the compose. So all the middlewares / enhancers will be called before we’re able to freeze them. A solution would be to have another store enhancer at the beginning, but it’s getting really cumbersome. So we’re coming with a custom compose function, which implements the extension’s store enhancers. There will be no confusion anymore regarding which enhancer should be the first and the last in the compose.

Instead of window.devToolsExtension (which was planned to be deprecated in favour of window.__REDUX_DEVTOOLS_EXTENSION__), now we’ll be using window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__:

import { createStore, applyMiddleware, compose } from 'redux';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const enhancer = composeEnhancers(
applyMiddleware(...middleware),
// other store enhancers if any
);
const store = createStore(reducer, enhancer);

When the extension is not installed, we’re using Redux’ compose here.

In case you don’t want to allow the extension in production, envify the code and use:

process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__

Even though we’re simplifying things here, when adding extension’s options, the usage of our new enhancer becomes complicated:

const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
name: 'MyApp', actionsBlacklist: ['REDUX_STORAGE_SAVE']
}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(...middleware),
// other store enhancers if any
);
const store = createStore(reducer, enhancer);

To make it easier, I’ve published a npm package to use it like that:

import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(reducer, composeWithDevTools(
applyMiddleware(...middleware),
// other store enhancers if any
));

or if needed to apply extension’s options:

import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
const composeEnhancers = composeWithDevTools({
name: 'MyApp', actionsBlacklist: ['REDUX_STORAGE_SAVE']
});
const store = createStore(reducer, composeEnhancers(
applyMiddleware(...middleware),
// other store enhancers if any
));

There’re just few lines of code. If you don’t want to allow the extension in production, just use ‘redux-devtools-extension/developmentOnly’ instead of ‘redux-devtools-extension’.

Compare with the old deprecated API:

import { createStore, applyMiddleware, compose } from 'redux';
const enhancer = compose(
applyMiddleware(...middleware),
// other store enhancers if any,
window.devToolsExtension ? window.devToolsExtension({
name: 'MyApp', actionsBlacklist: ['REDUX_STORAGE_SAVE']
}) : noop => noop
);
const store = createStore(reducer, enhancer);

Besides new features, now we don’t have the order’s and noop functions’ confusion, also the shared window namespace isn’t too vague anymore. The deprecation warnings will come in the next versions and there will be also more sugar for the npm package, so I highly recommend to use it instead.


Follow me on Twitter at @mdiordiev and watch the extension’s repo to not miss any updates.

A single golf clap? Or a long standing ovation?

By clapping more or less, you can signal to us which stories really stand out.