redux-devtools/README.md

429 lines
19 KiB
Markdown
Raw Normal View History

2015-07-14 22:46:44 +03:00
Redux DevTools
=========================
A live-editing time travel environment for [Redux](https://github.com/rackt/redux).
2015-08-12 14:49:31 +03:00
**[See Dan's React Europe talk demoing it!](http://youtube.com/watch?v=xsSnOQynTHs)**
2015-07-15 00:45:02 +03:00
2015-09-24 17:58:42 +03:00
[![build status](https://img.shields.io/travis/gaearon/redux-devtools/master.svg?style=flat-square)](https://travis-ci.org/gaearon/redux-devtools)
[![npm version](https://img.shields.io/npm/v/redux-devtools.svg?style=flat-square)](https://www.npmjs.com/package/redux-devtools)
[![npm downloads](https://img.shields.io/npm/dm/redux-devtools.svg?style=flat-square)](https://www.npmjs.com/package/redux-devtools)
2015-12-13 02:33:35 +03:00
[![redux channel on discord](https://img.shields.io/badge/discord-redux@reactiflux-738bd7.svg?style=flat-square)](https://discord.gg/0ZcbPKXt5bWb10Ma)
2015-09-24 17:58:42 +03:00
2015-08-12 16:30:49 +03:00
![](http://i.imgur.com/J4GeW0M.gif)
2015-08-12 14:49:31 +03:00
### Features
* Lets you inspect every state and action payload
* Lets you go back in time by “cancelling” actions
2015-12-13 02:33:35 +03:00
* If you change the reducer code, each “staged” action will be re-evaluated
2015-08-12 14:49:31 +03:00
* If the reducers throw, you will see during which action this happened, and what the error was
* With `persistState()` store enhancer, you can persist debug sessions across page reloads
2015-07-15 01:05:08 +03:00
2015-12-14 07:03:54 +03:00
### Overview
2015-12-15 07:53:24 +03:00
Redux DevTools is a development time package that provides power-ups for your Redux development workflow. Be careful to strip its code in production! To use Redux DevTools, you need to choose a “monitor”—a React component that will serve as a UI for the DevTools. Different tasks and workflows require different UIs, so Redux DevTools is built to be flexible in this regard. We recommend using [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) for inspecting the state and time travel, and wrap it in a [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor) to quickly move it across the screen. That said, when youre comfortable rolling up your own setup, feel free to do this, and share it with us.
2015-12-14 07:03:54 +03:00
2015-07-15 00:45:02 +03:00
### Installation
2015-08-10 18:54:25 +03:00
```
npm install --save-dev redux-devtools
```
2015-07-15 00:45:02 +03:00
2015-12-14 07:03:54 +03:00
Youll also likely want to install some monitors:
```
npm install --save-dev redux-devtools-log-monitor
npm install --save-dev redux-devtools-dock-monitor
```
### Usage
#### Create a `DevTools` Component
2015-12-14 07:03:54 +03:00
Somewhere in your project, create a `DevTools` component by passing a `monitor` element to `createDevTools`. In the following example our `monitor` consists of [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) docked within [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor):
2015-12-13 02:33:35 +03:00
2015-12-14 07:03:54 +03:00
##### `containers/DevTools.js`
2015-08-23 14:17:33 +03:00
```js
2015-12-13 02:33:35 +03:00
import React from 'react';
2015-09-24 20:35:33 +03:00
2015-12-14 07:03:54 +03:00
// Exported from redux-devtools
2015-12-13 02:33:35 +03:00
import { createDevTools } from 'redux-devtools';
2015-09-24 20:35:33 +03:00
2015-12-14 07:03:54 +03:00
// Monitors are separate packages, and you can make a custom one
2015-09-24 20:35:33 +03:00
import LogMonitor from 'redux-devtools-log-monitor';
2015-12-13 02:33:35 +03:00
import DockMonitor from 'redux-devtools-dock-monitor';
2015-12-14 07:03:54 +03:00
// createDevTools takes a monitor and produces a DevTools component
const DevTools = createDevTools(
2015-12-14 07:55:09 +03:00
// Monitors are individually adjustable with props.
// Consult their repositories to learn about those props.
// Here, we put LogMonitor inside a DockMonitor.
2015-12-14 07:03:54 +03:00
<DockMonitor toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'>
<LogMonitor theme='tomorrow' />
2015-12-13 02:33:35 +03:00
</DockMonitor>
);
2015-12-14 07:03:54 +03:00
export default DevTools;
```
Note that you can use `LogMonitor` directly without wrapping it in `DockMonitor` if youd like to display the DevTools UI somewhere right inside your application:
```js
// If you'd rather not use docking UI, use <LogMonitor> directly
const DevTools = createDevTools(
<LogMonitor theme='solarized' />
);
```
2015-12-14 07:03:54 +03:00
#### Use `DevTools.instrument()` Store Enhancer
The `DevTools` component you created with `createDevTools()` has a special static method called `instrument()`. It returns a [store enhancer](http://rackt.github.io/redux/docs/Glossary.html#store-enhancer) that you need to use in development.
A store enhancer is a function that takes `createStore` and returns an enhanced version of it that you will use instead. You probably already used another store enhancer—[`applyMiddleware()`](http://redux.js.org/docs/api/applyMiddleware.html). Unlike `applyMiddleware()`, you will need to be careful to only use `DevTools.instrument()` in development environment, and never in production.
The easiest way to apply several store enhancers in a row is to use the [`compose()`](http://redux.js.org/docs/api/compose.html) utility function that ships with Redux. It is the same `compose()` that you can find in Underscore and Lodash. In our case, we would use it to compose several store enhancers into one: `compose(applyMiddleware(m1, m2, m3), DevTools.instrument())`.
Its important that you should add `DevTools.instrument()` *after* `applyMiddleware` in your `compose()` function arguments. This is because `applyMiddleware` is potentially asynchronous, but `DevTools.instrument()` expects all actions to be plain objects rather than actions interpreted by asynchronous middleware such as [redux-promise](https://github.com/acdlite/redux-promise) or [redux-thunk](https://github.com/gaearon/redux-thunk). So make sure `applyMiddleware()` goes first in the `compose()` call, and `DevTools.instrument()` goes after it.
2015-12-13 02:33:35 +03:00
2015-12-14 07:54:39 +03:00
##### `store/configureStore.js`
```js
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from '../reducers';
import DevTools from '../containers/DevTools';
const finalCreateStore = compose(
// Middleware you want to use in development:
applyMiddleware(d1, d2, d3),
// Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument()
)(createStore);
export default function configureStore(initialState) {
const store = finalCreateStore(rootReducer, initialState);
// Hot reload reducers (requires Webpack or Browserify HMR to be enabled)
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers')/*.default if you use Babel 6+ */)
);
}
return store;
}
```
2015-12-14 07:03:54 +03:00
If youd like, you may add another store enhancer called `persistState()`. It ships with this package, and it lets you serialize whole sessions (including all dispatched actions and the state of the monitors) by a URL key. So if you visit `http://localhost:3000/?debug_session=reproducing_weird_bug`, do something in the app, then open `http://localhost:3000/?debug_session=some_other_feature`, and then go back to `http://localhost:3000/?debug_session=reproducing_weird_bug`, the state will be restored. The implementation of `persistState()` is fairly naïve but you can take it as an inspiration and build a proper UI for it if you feel like it!
2015-12-14 07:54:39 +03:00
```js
// ...
import { persistState } from 'redux-devtools';
const finalCreateStore = compose(
// Middleware you want to use in development:
applyMiddleware(d1, d2, d3),
// Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument(),
// Optional. Lets you write ?debug_session=<key> in address bar to persist debug sessions
persistState(getDebugSessionKey())
)(createStore);
function getDebugSessionKey() {
// You can write custom logic here!
// By default we try to read the key from ?debug_session=<key> in the address bar
const matches = window.location.href.match(/[?&]debug_session=([^&]+)\b/);
return (matches && matches.length > 0)? matches[1] : null;
}
export default function configureStore(initialState) {
// ...
}
```
2015-12-14 07:03:54 +03:00
#### Exclude DevTools from Production Builds
Finally, to make sure were not pulling any DevTools-related code in the production builds, we will envify our code. With Webpack, you can use `DefinePlugin` (Browserify equivalent is called [`envify`](https://github.com/zertosh/loose-envify)) to turn magic constants like `process.env.NODE_ENV` into `'production'` or `'development'` strings depending on the environment, and import and render `redux-devtools` conditionally when `process.env.NODE_ENV` is not `'production'`. Then, if you have an Uglify step before production, Uglify will eliminate dead `if (false)` branches with `redux-devtools` imports.
If you are using ES6 modules with Webpack 1.x and Babel, you might try putting your `import` statement inside an `if (process.env.NODE_ENV !== 'production)` to exclude the DevTools package from your production bundle. However this ES6 specification forbids it, so this wont compile. Instead, you can use a conditional CommonJS `require`. Babel will let it compile, and Uglify will eliminate the dead branches before Webpack creates a bundle. This is why we recommend creating a `configureStore.js` file that either directs you to `configureStore.dev.js` or `configureStore.prod.js` depending on the configuration. While it is a little bit more maintenance, the upside is that you can be sure you wont pull any development dependencies into the production builds, and that you can easily enable different middleware (e.g. crash reporting, logging) in the production environment.
##### `store/configureStore.js`
```js
// Use ProvidePlugin (Webpack) or loose-envify (Browserify)
// together with Uglify to strip the dev branch in prod build.
if (process.env.NODE_ENV === 'production') {
module.exports = require('./configureStore.prod');
} else {
module.exports = require('./configureStore.dev');
}
```
##### `store/configureStore.prod.js`
2015-08-23 14:17:33 +03:00
```js
2015-12-14 07:03:54 +03:00
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from '../reducers';
const finalCreateStore = compose(
// Middleware you want to use in production:
applyMiddleware(p1, p2, p3),
// Other store enhancers if you use any
)(createStore);
2015-12-13 02:33:35 +03:00
2015-12-14 07:03:54 +03:00
export default function configureStore(initialState) {
return finalCreateStore(rootReducer, initialState);
};
```
##### `store/configureStore.dev.js`
```js
import { createStore, applyMiddleware, compose } from 'redux';
2015-12-13 02:33:35 +03:00
import { persistState } from 'redux-devtools';
import rootReducer from '../reducers';
import DevTools from '../containers/DevTools';
const finalCreateStore = compose(
2015-12-14 07:03:54 +03:00
// Middleware you want to use in development:
applyMiddleware(d1, d2, d3),
// Required! Enable Redux DevTools with the monitors you chose
2015-12-13 02:33:35 +03:00
DevTools.instrument(),
2015-12-14 07:03:54 +03:00
// Optional. Lets you write ?debug_session=<key> in address bar to persist debug sessions
persistState(getDebugSessionKey())
2015-09-01 04:40:45 +03:00
)(createStore);
2015-12-14 07:03:54 +03:00
function getDebugSessionKey() {
// You can write custom logic here!
// By default we try to read the key from ?debug_session=<key> in the address bar
const matches = window.location.href.match(/[?&]debug_session=([^&]+)\b/);
return (matches && matches.length > 0)? matches[1] : null;
}
2015-12-13 02:33:35 +03:00
export default function configureStore(initialState) {
const store = finalCreateStore(rootReducer, initialState);
2015-12-14 07:03:54 +03:00
// Hot reload reducers (requires Webpack or Browserify HMR to be enabled)
2015-12-13 02:33:35 +03:00
if (module.hot) {
module.hot.accept('../reducers', () =>
2015-12-14 07:03:54 +03:00
store.replaceReducer(require('../reducers')/*.default if you use Babel 6+ */)
2015-12-13 02:33:35 +03:00
);
}
return store;
}
```
2015-12-14 07:21:34 +03:00
#### Render `<DevTools>` in Your App...
2015-12-14 07:03:54 +03:00
Finally, include the `DevTools` component in your page.
A naïve way to do this would be to render it right in your `index.js`:
2015-12-13 02:33:35 +03:00
2015-12-14 07:03:54 +03:00
##### `index.js`
2015-12-13 02:33:35 +03:00
```js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
2015-12-14 07:03:54 +03:00
import TodoApp from './containers/TodoApp';
// Don't do this! Youre bringing DevTools into the production bundle.
import DevTools from './containers/DevTools';
2015-12-13 02:33:35 +03:00
const store = configureStore();
render(
<Provider store={store}>
<div>
<TodoApp />
<DevTools />
</div>
</Provider>
document.getElementById('app')
);
```
2015-12-14 07:03:54 +03:00
We recommend a different approach. Create a `Root.js` component that renders the root of your application (usually some component surrounded by a `<Provider>`). Then use the same trick with conditional `require` statements to have two versions of it, one for development, and one for production:
2015-12-13 02:33:35 +03:00
2015-12-14 07:03:54 +03:00
##### `containers/Root.js`
2015-12-13 02:33:35 +03:00
```js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./Root.prod');
} else {
module.exports = require('./Root.dev');
}
```
2015-12-14 07:03:54 +03:00
##### `containers/Root.dev.js`
2015-08-23 14:17:33 +03:00
```js
2015-12-13 02:33:35 +03:00
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import TodoApp from './TodoApp';
import DevTools from './DevTools';
2015-08-23 14:17:33 +03:00
export default class Root extends Component {
render() {
2015-12-13 02:33:35 +03:00
const { store } = this.props;
return (
2015-12-13 02:33:35 +03:00
<Provider store={store}>
<div>
<TodoApp />
<DevTools />
</div>
</Provider>
);
}
}
```
2015-12-14 07:03:54 +03:00
##### `containers/Root.prod.js`
2015-07-15 00:15:04 +03:00
2015-12-13 02:33:35 +03:00
```js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import TodoApp from './TodoApp';
export default class Root extends Component {
render() {
const { store } = this.props;
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}
}
```
2015-12-14 07:21:34 +03:00
#### ...Or Open Them in a New Window
2015-12-14 07:03:54 +03:00
2015-12-14 07:50:53 +03:00
When you use [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor), you usually want to render `<DevTools>` at the root of your app. It will appear in a docked container above it. However, you can also render it anywhere else in your React component tree. To do this, you can remove `DockMonitor` and instead render `<DevTools>` inside some component of your app. Dont forget to create two versions of this component to exclude `DevTools` in production!
2015-12-14 07:03:54 +03:00
2015-12-14 07:50:53 +03:00
However you dont even have to render `<DevTools>` in the same window. For example, you may prefer to display it in a popup. In this case, you can remove `DockMonitor` from `DevTools.js` and just use the `LogMonitor`, and have some code like this:
2015-12-14 07:03:54 +03:00
##### `index.js`
```js
import React from 'react';
import { Provider } from 'react-redux';
import { render } from 'react-dom';
import configureStore from './store/configureStore';
import App from './containers/App';
const store = configureStore();
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
if (process.env.NODE_ENV !== 'production') {
const showDevTools = require('./showDevTools');
showDevTools(store);
}
```
##### `showDevTools.js`
```js
import React from 'react';
import { render } from 'react-dom';
import DevTools from './containers/DevTools';
export default function showDevTools(store) {
const popup = window.open(null, 'Redux DevTools', 'menubar=no,location=no,resizable=yes,scrollbars=no,status=no');
// Reload in case it already exists
popup.location.reload();
setTimeout(() => {
popup.document.write('<div id="react-devtools-root"></div>');
render(
<DevTools store={store} />,
popup.document.getElementById('react-devtools-root')
);
}, 10);
}
```
Personal preferences vary, and whether to put the DevTools in a separate window, in a dock, or right inside you apps user interface, is up to you. Make sure to check the documentation for the monitors you use and learn about the different props they support for customizing the appearance and the behavior.
Note that there are no useful props you can pass to the `DevTools` component other than the `store`. The `store` prop is needed if you dont wrap `<DevTools>` in a `<Provider>`—just like with any connected component. To adjust the monitors, you need to pass props to them inside `DevTools.js` itself inside the `createDevTools()` call when they are used.
2015-10-17 14:15:59 +03:00
### Gotchas
* **Your reducers have to be pure and free of side effects to work correctly with DevTools.** For example, even generating a random ID in reducer makes it impure and non-deterministic. Instead, do this in action creators.
2015-12-14 07:18:29 +03:00
* **Make sure to only apply `DevTools.instrument()` and render `<DevTools>` in development!** In production, this will be terribly slow because actions just accumulate forever. As described above, you need to use conditional `require`s and use `ProvidePlugin` (Webpack) or `loose-envify` (Browserify) together with Uglify to remove the dead code. Here is [an example](https://github.com/erikras/react-redux-universal-hot-example/) that adds Redux DevTools handling the production case correctly.
2015-07-15 00:15:04 +03:00
2015-12-14 07:18:29 +03:00
* **It is important that `DevTools.instrument()` store enhancer should be added to your middleware stack *after* `applyMiddleware` in the `compose`d functions, as `applyMiddleware` is potentially asynchronous.** Otherwise, DevTools wont see the raw actions emitted by asynchronous middleware such as [redux-promise](https://github.com/acdlite/redux-promise) or [redux-thunk](https://github.com/gaearon/redux-thunk).
2015-07-15 00:32:47 +03:00
### Running Examples
2015-12-14 07:18:29 +03:00
Clone the project:
2015-07-15 00:15:04 +03:00
```
git clone https://github.com/gaearon/redux-devtools.git
cd redux-devtools
2015-12-14 07:18:29 +03:00
```
Run `npm install` in the root folder:
```
2015-07-15 00:15:04 +03:00
npm install
2015-12-14 07:18:29 +03:00
```
Now you can open an example folder and run `npm install` there:
2015-07-15 00:15:04 +03:00
2015-12-14 07:18:29 +03:00
```
2015-12-14 07:03:54 +03:00
cd examples/counter # or examples/todomvc
2015-07-15 00:15:04 +03:00
npm install
2015-12-14 07:18:29 +03:00
```
Finally, run the development server and open the page:
```
2015-07-15 00:15:04 +03:00
npm start
open http://localhost:3000
```
2015-12-14 07:18:29 +03:00
Try clicking on actions in the log, or changing some code inside the reducers. You should see the action log re-evaluate the state on every code change.
2015-07-15 00:26:06 +03:00
2015-12-14 07:18:29 +03:00
Also try opening `http://localhost:3000/?debug_session=123`, click around, and then refresh. You should see that all actions have been restored from the local storage.
2015-07-15 00:26:06 +03:00
2015-08-12 14:49:31 +03:00
### Custom Monitors
2015-07-15 00:32:47 +03:00
2015-12-14 07:06:58 +03:00
**DevTools accepts monitor components so you can build a completely custom UI.** [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) and [`DockMonitor`](https://github.com/gaearon/redux-devtools-dock-monitor) are just examples of what is possible.
2015-07-15 00:59:35 +03:00
2015-08-12 14:49:31 +03:00
**[I challenge you to build a custom monitor for Redux DevTools!](https://github.com/gaearon/redux-devtools/issues/3)**
2015-07-15 00:45:02 +03:00
Some crazy ideas for custom monitors:
* A slider that lets you jump between computed states just by dragging it
* An in-app layer that shows the last N states right in the app (e.g. for animation)
* A time machine like interface where the last N states of your app reside on different Z layers
2015-12-14 07:06:58 +03:00
* Feel free to come up with and implement your own! Check [`LogMonitor`](https://github.com/gaearon/redux-devtools-log-monitor) `propTypes` to see what you can do.
2015-07-15 00:45:02 +03:00
2015-08-12 14:49:31 +03:00
In fact some of these are implemented already:
#### [redux-slider-monitor](https://github.com/calesce/redux-slider-monitor)
![](https://camo.githubusercontent.com/d61984306d27d5e0739efc2d57c56ba7aed7996c/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f662e636c2e6c792f6974656d732f3269314c3147316e3161316833793161324f31772f53637265656e2532305265636f7264696e67253230323031352d30382d3034253230617425323030372e3435253230504d2e676966)
#### [redux-devtools-filterable-log-monitor](https://github.com/bvaughn/redux-devtools-filterable-log-monitor/)
![reduxfilterablelogmonitor](https://cloud.githubusercontent.com/assets/29597/12024861/a510cb96-ad76-11e5-9ce3-78f46a2f6837.gif)
2015-08-12 14:49:31 +03:00
#### Keep them coming!
Create a PR to add your custom monitor.
2015-07-15 00:59:35 +03:00
2015-07-15 00:45:02 +03:00
### License
MIT