// Tutorial //

Using Storybook with React & Redux

Published on January 8, 2019
Default avatar
By Chris Dolphin
Developer and author at DigitalOcean.
Using Storybook with React & Redux

While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

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


Want to learn more? Join the DigitalOcean Community!

Join our DigitalOcean community of over a million developers for free! Get help and share knowledge in our Questions & Answers section, find tutorials and tools that will help you grow as a developer and scale your project or business, and subscribe to topics of interest.

Sign up
About the authors
Default avatar
Developer and author at DigitalOcean.

Still looking for an answer?

Was this helpful?
Leave a comment

This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!