Skip to content

Instantly share code, notes, and snippets.

@marc-rutkowski
Last active June 17, 2021 13:17
Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save marc-rutkowski/48fa77fdc9439588ce4a3665137fb018 to your computer and use it in GitHub Desktop.
Save marc-rutkowski/48fa77fdc9439588ce4a3665137fb018 to your computer and use it in GitHub Desktop.
react-storybook with react-boilerplate (content of the .storybook/ directory)

react-storybook with react-boilerplate

Explains how to install and configure react-storybook into a project created from the react-boilerplate setup.

Intro

React-boilerplate (rbp) is a good starting point to create React apps, with a focus on performance, best practices and DX.

The community around the repository is amazing and a constant source of learning.

In parallel react-storybook is a great tool to develop and vizualize React components into isolation.

It lets you create component stories in order to mount your components into a gallery-type UI. Each story allows to specify different property values, leading to vizualize the component into different states.

While the basic integration of react-storybook into any kind of project is very easy and the documentation helpful, this post gives some tips to help you with issues like webpack loaders and internationalization support into the context of a project created from the react-boilerplate setup.

Basic setup

The following assumes you have a working copy of react-boilerplate and its default example app.

  1. Install the getstorybook utility : npm i -g getstorybook
  2. Run it into your react-boilerplate project directory : cd my-react-boilerplate-project && getstorybook
  3. Start the storybook local server : npm run storybook
  4. Now you can navigate to http://localhost:6006/ and see the storybook UI and the default stories generated by the utility

At this point you should be able to change the default stories/index.js file created for you by getstorybook with the following code to show the boilerplate's Button component right into the Storybook UI (auto reloaded !)

import React from 'react';
import { storiesOf, action } from '@kadira/storybook';
import Button from '../app/components/Button';

storiesOf('Button', module)
.add('with text', () => (
  <Button onClick={action('clicked')}>Hello Button</Button>
));

This is easy isn't it? But unfortunately this works for the Button component, but not for some other components that are part of the react-boilerplate example app.

In the following section, we'll provide some additional details to create a good integration.

Webpack configuration

Storybook comes with its own Webpack configuration used to process the stories and mount components into the UI.

Obviously this configuration doesn't include all the things included into the react-boilerplate setup.

Let's modify again the stories/index.js file to include a story about the Header component of the rbp example, and see the problem by yourself.

import React from 'react';
import { storiesOf, action } from '@kadira/storybook';
import Button from '../app/components/Button';
import Header from '../app/components/Header';

storiesOf('Button', module)
.add('with text', () => (
  <Button onClick={action('clicked')}>Hello Button</Button>
));

storiesOf('Header', module)
.add('default', () => (
  <Header />
));

In this example the webpack image-loader that is already set into rbp will not be runned by the storybook building process, leading you to this kind of error message Module parse failed: (...)/app/components/Header/banner.jpg Unexpected character '�' (1:0) You may need an appropriate loader to handle this file type.

To fix this problem, you'll need to extend the default storybook webpack configuration to include the same loaders that are used into rbp development or production builds. See the storybook docs for more details.

The .storybook/webpack.config.js file can be modified to look like the following.

const path = require('path');
const baseDir = path.resolve(__dirname, '../app');

// load the dev config from react-boilerplate
const devConfig = require('../internals/webpack/webpack.dev.babel');

module.exports = function genConfig(storybookBaseConfig) {
  // concat loaders from dev config
  /* eslint-disable no-param-reassign */
  storybookBaseConfig.module.loaders = storybookBaseConfig.module
    .loaders.concat(devConfig.module.loaders);

  // add the "app" folder to the resolve list
  storybookBaseConfig.resolve.fallback.push(baseDir);

  // return the altered config
  return storybookBaseConfig;
};

This configuration does the following things:

  • Import and merge loaders settings from internals/webpack/webpack.dev.babel so you'll be sure that components displayed into your storybook will be processed by the same loaders (babel, style-loader, css-loader, file-loader…) that are already in use into the boilerplate.
  • Add the app folder to the webpack resolve fallback option to get correct imports resolution for your files.

You need to stop and restart storybook to apply this modification.

With this webpack configuration for storybook, you should get rid of the error about the Header image, but the component still cannot be rendered correctly and you'll see this message into the storybook UI : [React Intl] Could not find required 'intl' object. <IntlProvider> needs to exist in the component ancestry.

In the following section, we'll see how to avoid this last problem and mount our components that depends on React Intl.

Storybook settings

The .storybook/config.js file allows to configure how the stories are loaded and rendered into the Storybook UI.

Intl support

To render components that used the intl context we need to wrap the story into an IntlProvider component.

You can modify the stories file to include the IntlProvider wrapper, like in the following code (the Button story previously showned was removed for simplicity).

import React from 'react';
import { storiesOf } from '@kadira/storybook';
import { IntlProvider } from 'react-intl';
import { translationMessages } from '../app/i18n';
import Header from '../app/components/Header';

const DEFAULT_LOCALE = 'en';

storiesOf('Header', module)
.add('default', () => (
  <IntlProvider locale={DEFAULT_LOCALE} messages={translationMessages[DEFAULT_LOCALE]}>
    <Header />
  </IntlProvider>
));

It's all good now, and you can finally see the Header component into the storybook UI.

But I think it will be more useful to have this setting available for all our components.

Storybook provides an API to wrap all stories into whatever components you want before rendering them into the UI.

This can be done with the addDecorator function that can be used into the global storybook settings.

So you can update the .storybook/config.js file, like into the following snippet.

import React from 'react';
import { configure, addDecorator } from '@kadira/storybook';
import { IntlProvider } from 'react-intl';

// import translation messages
import { translationMessages } from '../app/i18n';

// add a decorator to wrap stories rendering into IntlProvider 
const DEFAULT_LOCALE = 'en';
addDecorator((story) => (
  <IntlProvider locale={DEFAULT_LOCALE} messages={translationMessages[DEFAULT_LOCALE]}>
    { story() }
  </IntlProvider>
));

// stories loader
function loadStories() {
  require('../stories');
}

// initialize react-storybook
configure(loadStories, module);

Now the Intl support will be available for all our stories, and the stories/index.js file can be reverted to something clearer.

import React from 'react';
import { storiesOf } from '@kadira/storybook';
import Header from './index';

storiesOf('Header', module)
.add('default', () => (
  <Header />
));

Stories location

Storybook settings also allow you to set where the tool will be looking for stories.

As the react-boilerplate enforces it, each part of a component (code, styles, tests) should stay in the same place. So you should probably want to keep your stories into each component folder.

With our current setup, storybook will load stories from the stories/index.js file.

Let's modify the "stories loader" part of the .storybook/config.js file to look for all files matching the pattern *.stories.js.

// stories loader
const req = require.context('../app', true, /.stories.js$/);
function loadStories() {
  req.keys().forEach((filename) => req(filename));
}

// initialize react-storybook
configure(loadStories, module);

You need to stop and restart storybook to apply this modification.

Now we can remove the example stories folder and create a new file named index.stories.js into app/components/Button to write stories for the Button component.

import React from 'react';
import { storiesOf, action } from '@kadira/storybook';
import Button from './index';

storiesOf('Button', module)
.add('with text', () => (
  <Button onClick={action('clicked')}>Hello Button</Button>
));

And another one into app/components/Header to write stories for the Header component.

import React from 'react';
import { storiesOf } from '@kadira/storybook';
import Header from './index';

storiesOf('Header', module)
.add('default', () => (
  <Header />
));

Info Addon

Stories definitions can be extended with addons. Available addons are listed into a gallery page.

I found the Info addon (used into the AirBnB react-dates showcase) very useful to display some information about the components and their stories.

Steps to integrate it into our setup are the following.

  • Stop the storybook if it's still running (ctrl-c).
  • Add the info addon package: yarn add --dev @kadira/react-storybook-addon-info
  • Edit the .storybook/config.js to import and connect the addon using the setAddon function.
import React from 'react';
import { configure, addDecorator, setAddon } from '@kadira/storybook';
import infoAddon from '@kadira/react-storybook-addon-info';

...

setAddon(infoAddon);

...
  • Rebuild the dependencies dll: npm run build:dll
  • Restart the storybook: npm run storybook
  • Now the story for the Button can be modified to use the addWithInfo function instead of the default add.
import React from 'react';
import { storiesOf, action } from '@kadira/storybook';
import Button from './index';

storiesOf('Button', module)
.addWithInfo('with text',
  `
    The default use case for the button component.
  `,
  () => (
    <Button onClick={action('clicked')}>Hello Button</Button>
));

The question mark button at the top-right gives access to an info page about the component, its properties and the story.

Bonus: grid background

Like the Info Addon, a nice grid background is rendered into the react-dates storybook.

If you want something similar into your project you can simply add an head.html file into the .storybook directory and fill it with the following content.

<style>
  body {
    background-color: rgba(0, 0, 0, 0.05);
    background-image: repeating-linear-gradient(0deg, transparent, transparent 7px, rgba(0, 0, 0, 0.2) 1px, transparent 8px), repeating-linear-gradient(90deg, transparent, transparent 7px, rgba(0, 0, 0, 0.2) 1px, transparent 8px);
    background-size: 8px 8px;
  }
</style>

Storybook injects tags from head.html into the iFrame used to render your stories/components.

View documentation

Gist

The configuration files are available into this Gist.

import React from 'react';
import { configure, addDecorator, setAddon } from '@kadira/storybook';
import { IntlProvider } from 'react-intl';
import infoAddon from '@kadira/react-storybook-addon-info';
// import translation messages
import { translationMessages } from '../app/i18n';
// add the Info Addon
setAddon(infoAddon);
// add a decorator to wrap stories rendering into IntlProvider
const DEFAULT_LOCALE = 'en';
addDecorator((story) => (
<IntlProvider locale={DEFAULT_LOCALE} messages={translationMessages[DEFAULT_LOCALE]}>
{ story() }
</IntlProvider>
));
// stories loader
const req = require.context('../app', true, /.stories.js$/);
function loadStories() {
req.keys().forEach((filename) => req(filename));
}
// initialize react-storybook
configure(loadStories, module);
<style>
body {
background-color: rgba(0, 0, 0, 0.05);
background-image: repeating-linear-gradient(0deg, transparent, transparent 7px, rgba(0, 0, 0, 0.2) 1px, transparent 8px), repeating-linear-gradient(90deg, transparent, transparent 7px, rgba(0, 0, 0, 0.2) 1px, transparent 8px);
background-size: 8px 8px;
}
</style>
/*
Webpack config for react-storybook
*/
const path = require('path');
const baseDir = path.resolve(__dirname, '../app');
// load the dev config
const devConfig = require('../internals/webpack/webpack.dev.babel');
module.exports = function genConfig(storybookBaseConfig) {
// concat loaders from dev config
/* eslint-disable no-param-reassign */
storybookBaseConfig.module.loaders = storybookBaseConfig.module
.loaders.concat(devConfig.module.loaders);
// add the "app" folder to the resolve list
storybookBaseConfig.resolve.fallback.push(baseDir);
// return the altered config
return storybookBaseConfig;
};
@mikeloll
Copy link

👍 Thank you!

@srdone
Copy link

srdone commented Feb 7, 2017

This is awesome! Thank you!

@jptissot
Copy link

Thanks a lot ! Do you have any idea how to make postcss work with react-boilerplate and react-storybook ?

@amirc
Copy link

amirc commented Mar 19, 2017

That is really cool, thanks.
I've added to components generator a story file
file: /internals/generators/component/index.js

module.exports = {
       path: '../../app/components/{{properCase name}}/tests/index.test.js',
       templateFile: './component/test.js.hbs',
       abortOnFail: true,
+    }, {
+      type: 'add',
+      path: '../../app/components/{{properCase name}}/{{properCase name}}.stories.js',
+      templateFile: './component/stories.js.hbs',
+      abortOnFail: true,
     }];

and in the new file internals/generators/component/stories.js.hbs

import React from 'react';
import { storiesOf } from '@kadira/storybook';
import {{ properCase name }} from './index';

storiesOf('{{ properCase name }}', module)
  .add('default', () => (
    <{{ properCase name }} />
  ));

@mattvpham
Copy link

I ran into an issue on Windows where storybook choked when I included the Header story. Here's the log:

λ  npm run storybook

> react-boilerplate@3.4.0 storybook D:\programming\playground\react-boilerplate
> start-storybook -p 9001 -c .storybook

@kadira/storybook v2.35.3

=> Loading custom webpack config (full-control mode).

React Storybook started on => http://localhost:9001/

D:\programming\playground\react-boilerplate\node_modules\enhanced-resolve\node_modules\memory-fs\lib\join.js:11
        if(absoluteWinRegExp.test(path)) return normalize(path + "\\" + request.replace(/\//g, "\\"));
                                                                               ^

TypeError: Cannot read property 'replace' of undefined
    at Tapable.join (D:\programming\playground\react-boilerplate\node_modules\enhanced-resolve\node_modules\memory-fs\lib\join.js:11:73)
    at Tapable.<anonymous> (D:\programming\playground\react-boilerplate\node_modules\enhanced-resolve\lib\FileAppendPlugin.js:14:19)
    at Tapable.applyPluginsParallelBailResult (D:\programming\playground\react-boilerplate\node_modules\tapable\lib\Tapable.js:139:14)
    at Tapable.<anonymous> (D:\programming\playground\react-boilerplate\node_modules\enhanced-resolve\lib\Resolver.js:103:8)
    at Tapable.Resolver.forEachBail (D:\programming\playground\react-boilerplate\node_modules\enhanced-resolve\lib\Resolver.js:196:3)
    at Tapable.doResolve (D:\programming\playground\react-boilerplate\node_modules\enhanced-resolve\lib\Resolver.js:102:7)
    at Tapable.resolve (D:\programming\playground\react-boilerplate\node_modules\enhanced-resolve\lib\Resolver.js:45:14)
    at Tapable.resolve (D:\programming\playground\react-boilerplate\node_modules\enhanced-resolve\lib\UnsafeCachePlugin.js:23:14)
    at D:\programming\playground\react-boilerplate\node_modules\@kadira\storybook\node_modules\webpack\lib\NormalModuleFactory.js:169:12
    at D:\programming\playground\react-boilerplate\node_modules\async\lib\async.js:356:13
    at async.forEachOf.async.eachOf (D:\programming\playground\react-boilerplate\node_modules\async\lib\async.js:233:13)
    at _asyncMap (D:\programming\playground\react-boilerplate\node_modules\async\lib\async.js:355:9)
    at Object.map (D:\programming\playground\react-boilerplate\node_modules\async\lib\async.js:337:20)
    at NormalModuleFactory.resolveRequestArray (D:\programming\playground\react-boilerplate\node_modules\@kadira\storybook\node_modules\webpack\lib\NormalModuleFactory.js:166:8)
    at D:\programming\playground\react-boilerplate\node_modules\async\lib\async.js:718:13
    at async.forEachOf.async.eachOf (D:\programming\playground\react-boilerplate\node_modules\async\lib\async.js:233:13)

This happened to me because react-boilerplate and react-storybook are on different versions of webpack. When the Header imports banner.jpg, storybook's webpack gets confused by the new-style image-webpack-loader entry.

I fixed this by removing image-webpack-loader from webpack.base.babel.js.

...
    }, {
      test: /\.(jpg|png|gif)$/,
      loaders: [
        'file-loader',
        // {
        //   loader: 'image-webpack-loader',
        //   query: {
        //     progressive: true,
        //     optimizationLevel: 7,
        //     interlaced: false,
        //     pngquant: {
        //       quality: '65-90',
        //       speed: 4,
        //     },
        //   },
        // },
      ],
    }, {
...

Hope this prevents the next poor soul from losing their hair.

@avdeev
Copy link

avdeev commented Jul 4, 2017

Mac-mini-Alexey:babo-admin alexeyavdeev$ yarn run storybook
yarn run v0.27.5
$ start-storybook -p 6006
@storybook/react v3.1.7

=> Loading custom webpack config (full-control mode).
/Users/alexeyavdeev/projects/babo-admin/.storybook/webpack.config.js:14
    .loaders.concat(devConfig.module.loaders);
            ^

TypeError: Cannot read property 'concat' of undefined
    at genConfig (/Users/alexeyavdeev/projects/babo-admin/.storybook/webpack.config.js:14:13)
    at exports.default (/Users/alexeyavdeev/projects/babo-admin/node_modules/@storybook/react/dist/server/config.js:61:12)
    at exports.default (/Users/alexeyavdeev/projects/babo-admin/node_modules/@storybook/react/dist/server/middleware.js:19:37)
    at Object.<anonymous> (/Users/alexeyavdeev/projects/babo-admin/node_modules/@storybook/react/dist/server/index.js:152:34)
    at Module._compile (module.js:569:30)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:503:32)
    at tryModuleLoad (module.js:466:12)
    at Function.Module._load (module.js:458:3)
    at Function.Module.runMain (module.js:605:10)
    at startup (bootstrap_node.js:158:16)
    at bootstrap_node.js:575:3
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

@bitIO
Copy link

bitIO commented Jul 4, 2017

@avdeev to me it looks like storybook has a newer version of webpack. This is my current configuration:

webpack.config.js

/*
  Webpack config for react-storybook
*/
const path = require('path');
const baseDir = path.resolve(__dirname, '../app');

// load the dev config
const devConfig = require('../internals/webpack/webpack.dev.babel');

module.exports = function genConfig(storybookBaseConfig) {
  // concat loaders from dev config
  /* eslint-disable no-param-reassign */
  storybookBaseConfig.module.rules = storybookBaseConfig.module
    .rules.concat(devConfig.module.loaders);

  // add the "app" folder to the resolve list
  storybookBaseConfig.resolve.modules.push(baseDir);

  // return the altered config
  return storybookBaseConfig;
};

config.js

import React from 'react';

import { configure, addDecorator, setAddon } from '@kadira/storybook';
import infoAddon from '@kadira/react-storybook-addon-info';
import IntlAddon from 'react-storybook-addon-intl';

import { IntlProvider } from 'react-intl';
import { addLocaleData } from 'react-intl';
import es from 'react-intl/locale-data/es';

import 'sanitize.css/sanitize.css';
import 'semantic-ui-css/semantic.min.css';
import '../app/global-styles';


// import translation messages
import { translationMessages } from '../app/i18n';
setAddon(IntlAddon);
addLocaleData(es);

// add the Info Addon
setAddon(infoAddon);

// add a decorator to wrap stories rendering into IntlProvider
const DEFAULT_LOCALE = 'en';
addDecorator((story) => (
  <IntlProvider locale={DEFAULT_LOCALE} messages={translationMessages[DEFAULT_LOCALE]}>
    { story() }
  </IntlProvider>
));

// stories loader
const req = require.context('../app/', true, /stories\.js$/);
function loadStories() {
  req.keys().forEach((filename) => req(filename));
}

// initialize react-storybook
configure(loadStories, module);

@weizenberg
Copy link

@bitIO

Just a small fix for the following error:

WebpackOptionsValidationError: Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.

You tried to load the loaders instead the rules, so by changing this line, this is the correct configurations:

/*
  Webpack config for react-storybook
*/
const path = require('path');
const baseDir = path.resolve(__dirname, '../app');

// load the dev config
const devConfig = require('../internals/webpack/webpack.dev.babel');

module.exports = function genConfig(storybookBaseConfig) {
  // concat loaders from dev config
  /* eslint-disable no-param-reassign */
  storybookBaseConfig.module.rules = storybookBaseConfig.module
    .rules.concat(devConfig.module.rules);

  // add the "app" folder to the resolve list
  storybookBaseConfig.resolve.modules.push(baseDir);

  // return the altered config
  return storybookBaseConfig;
};

@xkrsz
Copy link

xkrsz commented Apr 23, 2018

@weizenberg thank you! You saved me a while pointing out this mistake.

@taylorpreston
Copy link

WebpackOptionsValidationError: Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.

  • configuration.module.rules[2] should be an object.

Does anyone know where this error might be coming from? My best guess is it has to do with the different versions of webpack, but I could be completely wrong.

@sumantaparida
Copy link

Getting errorrules are undefined.

@sumantaparida
Copy link

Cannot read property 'rules' of undefined

@brenohq
Copy link

brenohq commented Sep 10, 2019

Im getting this error too:

Cannot read property 'rules' of undefined

anyone has an workaround?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment