Skip to content

Latest commit

 

History

History
384 lines (276 loc) · 13.2 KB

File metadata and controls

384 lines (276 loc) · 13.2 KB
id title
writing-stories
Writing Stories

A Storybook is a collection of stories. Each story represents a single visual state of a component.

Technically, a story is a function that returns something that can be rendered to screen.

Basic story

Here is an example of stories for a Button component:

import React from 'react';
import { action } from '@storybook/addon-actions';
import Button from './Button';

export default {
  component: Button,
  title: 'Button',
};

export const text = () => <Button onClick={action('clicked')}>Hello Button</Button>;

export const emoji = () => (
  <Button onClick={action('clicked')}>
    <span role="img" aria-label="so cool">
      😀 😎 👍 💯
    </span>
  </Button>
);

This is what you'll see in Storybook:

Basic stories

The named exports define the Button's stories, and the default export defines metadata that applies to the group. In this case, the component is Button. The title determines the title of the group in Storybook's left-hand navigation panel and should be unique, i.e. not re-used across files. In this case it's located at the top level, but typically it's positioned within the story hierarchy.

This example is written in Storybook's Component Story Format (CSF). Storybook also supports:

  • a classic storiesOf API, which adds stories through Storybook's API.
  • an experimental MDX syntax, which mixes longform Markdown docs and JSX stories.

Since CSF is a new addition to Storybook, most Storybook examples you'll find in the wild are written to the storiesOf API.

Furthermore, Storybook for React Native currently only supports the storiesOf format. React Native will get CSF and MDX support in a future release.

Story file location

Stories are easier to maintain when they are located alongside the components they document. We recommend:

•
└── src
    └── components
        └── button
            ├── button.js
            └── button.stories.js

It's up to you to find a naming/placing scheme that works for your project/team. Other naming conventions:

stories sub-directory in component directory
•
└── src
    └── components
        └── button
            ├── button.js
            └── stories
                └── button.stories.js
stories directory outside src directory
•
├── src
│   └── components
│       └── button.js
└── stories
    └── button.stories.js

Loading stories

Stories are loaded in the .storybook/main.js file or .storybook/preview.js file.

The most convenient way to load stories is by filename. For example, if your stories files are located in the src/components directory, you can use the following snippet:

// .storybook/main.js
module.exports = {
  stories: ['../src/components/**/*.stories.js'],
};

Alternatively you can import all your stories in .storybook/preview.js:

import { configure } from '@storybook/react';

configure(require.context('../src/components', true, /\.stories\.js$/), module);

NOTE: The configure function should be called only once in .storybook/preview.js.

The configure function accepts:

  • A single require.context "req"
  • An array of reqs to load from multiple locations
  • A loader function that should return void or an array of module exports

If you want to load from multiple locations, you can use an array:

import { configure } from '@storybook/react';

configure(
  [
    require.context('../src/components', true, /\.stories\.js$/),
    require.context('../lib', true, /\.stories\.js$/),
  ],
  module
);

Or if you want to do some custom loading logic, you can use a loader function. Just remember to return an array of module exports if you want to use Component Story Format. Here's an example that forces files to load in a specific order.

import { configure } from '@storybook/react';

const loaderFn = () => [
  require('./welcome.stories.js'),
  require('./prelude.stories.js'),
  require('./button.stories.js'),
  require('./input.stories.js'),
];

configure(loaderFn, module);

Here's another example that mixes manual loading with glob-style loading:

import { configure } from '@storybook/react';

const loaderFn = () => {
  const allExports = [require('./welcome.stories.js')];
  const req = require.context('../src/components', true, /\.stories\.js$/);
  req.keys().forEach(fname => allExports.push(req(fname)));
  return allExports;
};

configure(loaderFn, module);

Storybook uses Webpack's require.context to load modules dynamically. Take a look at the relevant Webpack docs to learn more about how to use require.context.

If you are using the storiesOf API directly, or are using @storybook/react-native where CSF is unavailable, you should use a loader function with no return value:

import { configure } from '@storybook/react';

const loaderFn = () => {
  // manual loading
  require('./welcome.stories.js');
  require('./button.stories.js');

  // dynamic loading, unavailable in react-native
  const req = require.context('../src/components', true, /\.stories\.js$/);
  req.keys().forEach(fname => req(fname));
};

configure(loaderFn, module);

Furthermore, the React Native packager resolves all imports at build-time, so it's not possible to load modules dynamically. There is a third party loader react-native-storybook-loader to automatically generate the import statements for all stories.

Decorators

A decorator is a way to wrap a story with a common set of components, for example if you want to wrap a story in some formatting, or provide some context to the story.

Decorators can be applied globally, at the component level, or individually at the story level. Global decorators are typically applied in the Storybook config files, and component/story decorators are applied in the story file.

Here is an example of a global decorator which centers every story in the .storybook/preview.js:

import React from 'react';
import { addDecorator } from '@storybook/react';

addDecorator(storyFn => <div style={{ textAlign: 'center' }}>{storyFn()}</div>);

* In Vue projects you have to use the special component <story/> instead of the function parameter storyFn that is used in React projects, even if you are using JSX, for example:

var decoratorVueJsx = () => ({ render() { return <div style={{ textAlign: 'center' }}><story/></div>} })
addDecorator(decoratorVueJsx)

var decoratorVueTemplate = () => ({ template: `<div style="text-align:center"><story/></div>` })
addDecorator(decoratorVueTemplate)

And here's an example of component/local decorators. The component decorator wraps all the stories in a yellow frame, and the story decorator wraps a single story in an additional red frame.

import React from 'react';
import MyComponent from './MyComponent';

export default {
  title: 'MyComponent',
  decorators: [storyFn => <div style={{ backgroundColor: 'yellow' }}>{storyFn()}</div>],
};

export const normal = () => <MyComponent />;
export const special = () => <MyComponent text="The Boss" />;
special.story = {
  decorators: [storyFn => <div style={{ border: '5px solid red' }}>{storyFn()}</div>],
};

Decorators are not only for story formatting, they are generally useful for any kind of context needed by a story.

  • Theming libraries require a theme to be passed in through context. Rather than redefining this in every story, add a decorator.
  • Likewise, state management libraries like Redux provide a global data store through context.
  • Finally, Storybook addons heavily use decorators. For example, the Storybook's Knobs addon uses decorators to modify the input properties of the story based on a UI.

Parameters

Parameters are custom metadata for a story. Like decorators, they can also be hierarchically applied: globally, at the component level, or locally at the story level.

Here's an example where we are annotating our stories with Markdown notes using parameters, to be displayed in the Notes addon.

We first apply some notes globally in the .storybook/preview.js.

import { load, addParameters } from '@storybook/react';
import defaultNotes from './instructions.md';

addParameters({ notes: defaultNotes });

This would make sense if, for example, instructions.md contained instructions on how to document your components when there is no documentation available.

Then for components that did have documentation, we might override it at the component/story level:

import React from 'react';
import MyComponent from './MyComponent';
import componentNotes from './notes.md';
import specialNotes from './special.md';

export default {
  title: 'MyComponent',
  parameters: { notes: componentNotes },
};

export const small = () => <MyComponent text="small" />;
export const medium = () => <MyComponent text="medium" />;
export const special = () => <MyComponent text="The Boss" />;
special.story = {
  parameters: { notes: specialNotes },
};

In this example, the small and medium stories get the component notes documented in notes.md (as opposed to the generic instructions in instructions.md). The special story gets some special notes.

Searching

By default, search results will show up based on the file name of your stories. As of storybook 5, you can extend this with notes to have certain stories show up when the search input contains matches. For example, if you built a Callout component that you want to be found by searching for popover or tooltip as well, you could use notes like this:

export const callout = () => <Callout>Some children</Callout>;
callout.story = {
  parameters: { notes: 'popover tooltip' },
};

Story hierarchy

Stories can be organized in a nested structure using "/" as a separator.

For example the following snippets nest the Button and Checkbox components within the Atoms group, under a top-level heading called Design System.

// Button.stories.js
import React from 'react';
import Button from './Button';

export default {
  title: 'Design System/Atoms/Button',
};
export const normal = () => <Button onClick={action('clicked')}>Hello Button</Button>;
// Checkbox.stories.js
import React from 'react';
import Checkbox from './Checkbox';

export default {
  title: 'Design System/Atoms/Checkbox',
};
export const empty = () => <Checkbox label="empty" />;
export const checked = () => <Checkbox label="checked" checked />;

By default the top-level heading will be treated as any other group, but if you'd like it to be given special emphasis as a "root", use the showRoots config option. See the configuration options parameter page to learn more.

Generating nesting path based on __dirname

Nesting paths can be programmatically generated with template literals because story names are strings.

One example would be to use base from paths.macro:

import React from 'react';
import base from 'paths.macro';
import BaseButton from '../components/BaseButton';

export default {
  title: `Other/${base}/Dirname Example`,
};
export const story1 = () => <BaseButton label="Story 1" />;
export const story2 = () => <BaseButton label="Story 2" />;

This uses babel-plugin-macros.

Run multiple storybooks

Multiple storybooks can be built for different kinds of stories or components in a single repository by specifying different port numbers in the start scripts:

{
  "scripts": {
    "start-storybook-for-theme": "start-storybook -p 9001 -c .storybook-theme",
    "start-storybook-for-app": "start-storybook -p 8001 -c .storybook-app"
  }
}

Permalinking to stories

Sometimes you might wish to change the name of a story or its position in the hierarchy, but preserve the link to the story or its documentation. Here's how to do it.

Consider the following story:

export default {
  title: 'Foo/Bar',
};

export const Baz = () => <MyComponent />;

Storybook's ID-generation logic will give this the ID foo-bar--baz, so the link would be ?path=/story/foo-bar--baz.

Now suppose you want to change the position in the hierarchy to OtherFoo/Bar and the story name to Moo. Here's how to do that:

export default {
  title: 'OtherFoo/Bar',
  id: 'Foo/Bar', // or 'foo-bar' if you prefer
};

export const Baz = () => <MyComponent />;
Baz.story = {
  name: 'Moo',
};

Storybook will prioritize the id over the title for ID generation, if provided, and will prioritize the story.name over the export key for display.