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.
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.
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.
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.
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)
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.
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.
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
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
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
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.
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!