Using Storybook with React & Redux

Chris Dolphin

Storybook allows you to develop UI components in isolation, which can improve component reuse, testability, and development speed.

Along with these developer experience (DX) benefits, Storybook works nicely with traditional component development and doesn't require changes to core functionality to display, test, and document your component library.

It can also provide further extension through “addons” and “decorators”. With Addons, the possibilities to mesh Storybook with your development flow are unlocked. Decorators then add the customization that your components need to run smoothly.

Storybook with React

Getting React up and running with Storybook is relatively simple. The full setup instructions are available in the official docs. This process will provide a .storybook/ directory and a config.js file. Once those pieces are in place, creating stories for React components is straightforward.

//in Button.story.js

import React from 'react'
import { storiesOf } from '@storybook/react'

import Button from './Button'

storiesOf('Button', module)
  .add('default', () => (
    <Button>Submit</Button>
  ))
//in .storybook/config.js

import { configure } from '@storybook/react'

configure(function() {
  require('../src/components/Button.story')
}, module)
$ npm run storybook

Running the command to launch Storybook will show the new story in the Stories Panel.

Provider component

Because Storybook presents our components in isolation, there can be problems recreating some of the functionality that our App might provide at a high-level, commonly found in Root.js or App.js. In Redux this would be a Provider, but this also applies to the Router from react-router, and any Provider from libraries like react-i18next or ThemeProviders from libraries like material-ui.

To hook this up without modifying component code we’ll use a helper provided by Storybook to create Decorators. Decorators allow components to be wrapped in common code, avoiding redundancies and inconsistency.

Before diving into decorators, we’re going to create a Provider component from all our high-level Providers. This could be as simple as exporting Redux Provider, but it can also include the Router, internationalization setup, etc.

//in Provider.js

import React from 'react'
import { Router } from 'react-router'
import { Provider } from 'react-redux'

const ProviderWrapper = ({ children, store }) => (
  <Provider store={store}>
    <Router>
      { children }
    </Router>
  </Provider>
)

export default ProviderWrapper

With the Provider separated out from the Root, it can easily be imported into any component story.

Along with importing the Provider we need to import and pass along an instance of our Redux store.

//in Button.story.js

...

import { storiesOf } from '@storybook/react'

import Provider from '../Provider.js'
import configureStore from '../configuedStore.js'

const store = configureStore()

...

storiesOf('Button', module)

This provides the story with easy access to store.dispatch so Redux actions can be called to simulate state changes in our component.

Decorators

Storybook Decorators allow stories to be wrapped in common code that the component might need to perform correctly. A common example is a wrapper to center a component in the page, but we’ll use a decorator to inject the Provider component into our stories, much like a HOC.

A simple implementation would be as follows.

//in Button.story.js

// ...

import Provider from '../Provider.js'
import configureStore from '../configuedStore.js'

const store = configureStore()

const withProvider = (story) => (
  <Provider store={store}>
    { story() }
  </Provider>
)

storiesOf('Button', module)
  .addDecorator(withProvider)
  .add('default', () => (
    <Button>Submit</Button>
  ))

This withProvider decorator can be turned into a utility function for use in other stories. For convenience, these decorators are normally kept in a new .storybook/decorators.js file so they can be imported throughout the codebase.

Global Decorators

As an additional help, Storybook provides a way to easily apply decorators to all stories globally. This can be accomplished by calling addDecorator in .storybook/config.js

//in .storybook/config.js

import { configure, addDecorator } from '@storybook/react'
import { withProvider } from './decorators'

// ...

addDecorator(withProvider)

DevTools

Redux DevTools

Redux DevTools is a “power-up” when developing with Redux, providing visuals for state changes, action replays, and more.

After installing the browser extension, there is some code configuration needed in the store.

//in configureStore.js

import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'

const store = createStore(reducer, composeWithDevTools(
  applyMiddleware(...middleware),
))

In the past Redux DevTools has had trouble working with Storybook as it isolates all components into an iframe. This prevented the extension from listening to the Redux actions within our stories.

Even though this isn’t as much of an issue anymore, there are a few Addons that were created to help.

Addons

Storybook Native Addons add further development functionality to Storybook. These can be extensions written by the community or custom to your development flow. As part of their configuration, addons are registered in a new .storybook/addons.js file.

storybook-addon-redux-listener

Before the issue was fixed more directly, this Redux DevTools listener addon reestablished the components’ connection to the browser extension. It also adds a simple panel to Storybook itself.

Configuration looks something like this:

//in .storybook/addons.js

import 'storybook-addon-redux-listener/register'
//in configureStore.js

import createStorybookListener from 'storybook-addon-redux-listener'

...

const middlewares = []

// OPTIONAL: attach when Storybook is active
if (process.env.NODE_ENV === 'storybook') {
  const reduxListener = createStorybookListener()
  middlewares.push(reduxListener)
}

const createStoreWithMiddleware = (reducers) => {
  return createStore(reducers, applyMiddleware(...middlewares))
}

const configureStore = () => createStoreWithMiddleware(reducers)

export default configureStore

addon-redux

This addon adds multiple custom panels that recreate some of the functionality from the DevTools.

//in .storybook/addons.js

import addons from '@storybook/addons'
import registerRedux from 'addon-redux/register'

registerRedux(addons)
//in configureStore.js

import { createStore, compose } from 'redux'
import reducer from './your/reducer'
import withReduxEnhancer from 'addon-redux/enhancer'

const configureStore = () => createStore(reducer, withReduxEnhancer)

export default configureStore

See Also

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#