Storybook - An alternative approach
Hiya đ, if you like Storybook youâve come to the right place. I loooooooove Storybook!! It is in my opinion the ONLY way to develop a UI, butâŠ
Since the v6.0.0
release I noticed one or two changes to how Storybook recommend writing .stories
I recently discussed this with Michael Shilman in a GitHub issue and wanted to follow up with some alternative approaches to that shown in the docs.
This isnât to say my suggestions are better but there are a few things that I feel itâs important to understand before using the Storybook recommended approach verbatim.
This is mainly because you might not need to use the recommended approach and sometimes a more simplified approach to writing code is actually more beneficial.
I know as JavaScript developers we have a habit of using complicated solutions to solve really simple problems⊠and I appreciate that without overly complicated code examples we canât punishingly set technical tests for new hires, but in Storybook world thereâs an opportunity to cut through the BS and attempt to provide meaningful and actually helpful examples of the code we write.
Personally I think this in itself is a skill, but then again I always have to google Array.prototype.reduce() so maybe donât listen to me!
On that note, if anyone on the Storybook team is reading this, I would like to make it abundantly clear none of the following should be considered criticisms⊠I love the project and you are all super brill brills and lovely to communicate and collaborate with!
Phew, panic over!
Start here
Iâm a React developer so most of what Iâll be discussing relates to Storybook React but perhaps some of the methods will be framework agnostic. đ€·ââïž
Iâll start with the basics.
1. There are x2 types of .stories
- CSF = Component Story Format đ„
- MDX = Markdown on steroids đȘ
The CSF is, for the most part the most common but there are some advantages to using MDX.
CSF đ„
I typically use CSF to document components I build as part of components libraries which are used across a number of applications and I go to great lengths to document the props and their usage examples to aid developers implementing the library in using and composing the components. I will also use this format for components developed for use in a single project/app for the same reasons. Iâm a contractor so once my work is done Iâm outta there đââïž leaving permanent members of the team to maintain what I create.
MDX đȘ
MDX stories are good for long form documentation and users familiar with markdown may well enjoy the ability to write and display components in the same file. I find this format less useful if the component is part of a component library FYI
That said I have used the MDX approach in MDX Embed which is a kind of component library but because the entire project is for use with MDX it made sense to use the MDX format. Prop tables can of course be used to document props in this format too.
2. Where to put your .stories
.
If youâve used the npx sb init
install youâll notice your .stories
are placed in a stories dir somewhere on the
root of your project. I suspect this is the default because the Storybook team donât wish to be opinionated about where
you put your .stories
I will be opinionated about this and state you should co-locate your .stories
with your components, for the simple
fact that đ
Files you edit together, should live together.
On any typical project this will be my directory structure and itâs what Iâll be working with in this post. I should
also point out any Storybook specific config will be in reference to @storybook/react": "^6.0.0
|-- src
|-- components
|--mr-button
|-- mr-button.js | .tsx
|-- mr-button.stories.js | .mdx | .tsx
Now that your .stories
are co-located with your components youâll need to let Storybook know what the file extensions
are and where to find them. The below will find all files with the .stories.js
, .stories.mdx
or .stories.tsx
file
extension in any sub-directory of src/components
.
Your exact config will differ if your directory structure is different from the above
// .storybook/main.js
module.exports = {
stories: ['../src/components/**/*.stories.@(js|mdx|tsx)'],
};
The next slightly confusing part of the Storybook setup is the difference between Canvas and Docs.
- âcanvasâ = A single page to display a single story and the addons panel
- âdocsâ = A single page to display multiple stories, the prop table and no addons panel
In this post weâll mostly be focussing on the âdocsâ tab and for this weâll need to ensure weâve installed the correct addons and configured Storybook correctly.
First install the required addons
npm install @storybook/addon-actions @storybook/addon-docs --save -dev
On this note. The Storybook default install npx sb init
comes with @storybook/addon-links
you can remove this if
you like, as we wonât be using it.
// .storybook/main.js
module.exports = {
stories: ["../src/components/**/*.stories.@(js|mdx|tsx)"],
+ addons: ['@storybook/addon-actions', '@storybook/addon-docs'],
}
Iâll circle back to addon-actions
later. We will be using it so donât worry about it for the moment.
TypeScript setup
If youâre a TypeScript user thereâs also a little bit of config required. Storybook does work with .tsx
files out of
the box but to generate prop tables from your interface
declarations youâll need to install and then add
react-docgen-typescript
to the Storybook config.
npm install typescript react-docgen-typescript --save -dev
// .storybook/main.js
module.exports = {
stories: ["../src/components/**/*.stories.@(js|mdx|tsx)"],
addons: ['@storybook/addon-actions', '@storybook/addon-docs'],
+ typescript: {
+ reactDocgen: 'react-docgen-typescript',
+ reactDocgenTypescriptOptions: {
+ shouldExtractLiteralValuesFromEnum: true,
+ shouldExtractValuesFromUnion: true,
+ propFilter: (prop) =>
+ prop.parent ? !/node_modules/.test(prop.parent.fileName) : true
+ }
+ }
}
The full set of typescript
options can be found here
but the main one to explain is the propFilter
This might make more sense if I show you an interface for a <button />
component
// components/mrs-button/mrs-button.tsx
import { HTMLAttributes } from 'react';
interface MrsButtonProps extends HTMLAttributes<HTMLButtonElement> {}
You can see from the above MrsButtonProps
extends HTMLAttributes<HTMLButtonElement>
and as youâll probably know HTML
<button />
elements have their own set of âpropsâ or rather,
attributes. We donât want react-docgen-typescript
to grab all of the
default HTML attributes and display them in Storybooksâ prop table so, we can filter them out by determining if they
come from node_modules
⊠because when you install React as a node module thatâs where HTMLAttributes
come from.
Now we have Storybook configured we can start to write stories, but before that weâll need a component.
Hereâs a snippet for some example components that Iâve amusingly named <MrButton/>
and <MrsButton />
MrButton = Javascript + propTypes
// components/mr-button/mr-button.js
import React from 'react';
import PropTypes from 'prop-types';
export const MrButton = ({ children, isBoogyTime, ...props }) => (
<button {...props} style={{ width: '100%' }}>
{isBoogyTime ? <span role='img'>đș </span> : null}
{children}
</button>
);
MrButton.propTypes = {
/** Displays man dancing emoji */
isBoogyTime: PropTypes.bool,
};
MrButton.defaultProps = {
isBoogyTime: false,
};
MrsButton = TypeScript + interface
// components/mrs-button/mrs-button.tsx
import React, { FunctionComponent, HTMLAttributes } from 'react';
interface MrsButtonProps extends HTMLAttributes<HTMLButtonElement> {
/** Displays man dancing emoji */
isBoogyTime?: boolean;
}
export const MrsButton: FunctionComponent<MrsButtonProps> = ({ children, isBoogyTime = false, ...props }) => (
<button {...props} style={{ width: '100%' }}>
{isBoogyTime ? <span role='img'>đș </span> : null}
{children}
</button>
);
Ok, brills, now we have a component we can begin to document itâs usage. The .stories
snippets below are only for
<MrButton />
which is .js
but it would be the same for <MrsButton />
which is .tsx
// components/mr-button/mr-button.stories.js
import React from 'react';
import { MrButton } from './mr-button';
export default {
title: 'components/MrButton',
parameters: {
component: MrButton,
componentSubtitle: 'The MrButton component is of html type button',
},
};
export const Usage = () => <MrButton>Mr Button</MrButton>;
This is how I like to start when documenting any component, and with a bit of luck the component in question wonât require any props making the âShow codeâ snippet easy to read, not to mention easier to implement, naturally this changes on a case by case basis.
Letâs walk through this from top to bottom and iâll explain a few things along the way.
export default {}
- This is as youâd expect the default object to export, Storybook knows what to do with this but we can give it a few object keys which might make for a better story
title:
- This is the title of the story as seen in Storybooksâ sidebar navigation, you can use slash separated values in this string and Storybook will turn those into a multi-level navigation items, I think this is really cool by the way! The above will results in a story called âMrButtonâ being nested under a navigation item called âcomponentsâ
parameters:{âŠ}
- The
component
key is used by Storybook to generate the prop table for the component you name here - The
componentSubtitle
is the text that will appear under the main heading when youâre on the âdocsâ tab.
export const Usage = () => {}
- You could name this first story anything you want, my preference is to call it Usage because itâs usually an example of how to use the component iâm documenting.
You might have noticed that Iâve not used the Template.bind({})
method as shown in the Storybook docs⊠donât worry
about that at the moment⊠I just want to demonstrate that you donât have to use it if you donât want to, and you
shouldnât use it if youâre not clear on what it does.
The above is a very simple example but I wanted to introduce the parameters
object which isnât currently fully
documented.
Documenting props
This isnât by any means the only way to use Storybook but my preference is to name each story in accordance with the
prop
youâre attempting to document.
For example đ
// components/mr-button/mr-button.stories.js
...
export const IsBoogyTime = () => (
<MrButton isBoogyTime={true}>Click me</MrButton>
);
⊠however đ€
Storybook by default will take the const name e.g IsBoogyTime
and convert it using Lodashâs
startCase, which results in Is Boogy Time
being used in the sidebar navigation.
On that note Storybook also recommend you start const names with a capital letter.
Thatâs cool but, in this case the prop weâre documenting is named isBoogyTime
and it might be kinda cool to ensure the
sidebar navigation item correctly represents the prop name
If youâre that way inclined you can use storyName
to manually determine what the sidebar navigation items are
For example đ
// components/mr-button/mr-button.stories.js
...
export const IsBoogyTime = () => (
<MrButton isBoogyTime={true}>Click me</MrButton>
);
+ IsBoogyTime.storyName = 'isBoogyTime'
Documenting props continued
As explained above Storybook has this mysterious parameters
object but what youâll see in the docs is
parameters: {...}
I hope youâll find this next bit helpful.
// components/mr-button/mr-button.stories.js
export const IsBoogyTime = () => (
<MrButton isBoogyTime={true}>Click me</MrButton>
);
IsBoogyTime.storyName = 'isBoogyTime'
+ IsBoogyTime.parameters = {
+ docs: {
+ description: {
+ story: 'The `isBoogyTime` prop displays a man dancing emoji'
+ }
+ }
};
docs:{âŠ}
This object has special powers and is whatâs used to pass component documentation on to the âdocsâ page.
description.story
Itâs here where you can document your individual .stories
, itâs a bit like the componentSubtitle
outlined above and
will appear under the title of each of your .stories
. I like to use this to provide further information about the prop
or prop usage.
Documenting props Show code
The reason I prefer to name my .stories
using the storyName
and the reason I like to use description.story
is so
that everything works in harmony when a user clicks the âShow codeâ button in the bottom right of the Storybook preview
panel.
Here theyâll see a code snippet of the component and the props that drive the rendered output seen in the preview panel.
For example đ
// "Show code"
() => <MrButton isBoogyTime={true}>Click me</MrButton>;
This for me completes the circle, and fundamentally is the whole point in writing documentation.
Storybook is so good at providing ways for developers to document and display component usage but ultimately it falls on
whoever is writing the .stories
to ensure the navigation name, story name, story description and code snippet align.
Itâs no small ask either. If youâre developing something, anything, then you already know how it works and it is hard to pop a different hat on and think about what it might be like if you were looking at these docs for the first timeâŠ
I find a good litmus test for this is to down 7 or 8 pints of Ale and see if I can build a UI using only the code iâve copied from the âShow codeâ panel⊠to note, Iâm not recommending this approach but it has served me well over the years đș
Beyond the basics
There are of course many scenarios where your .stories
primary focus isnât to document the components usage instead,
you may like to add your brands preferred method of using the component, e.g, âThe primary button should only be used on
the checkout pageâ. You can also use Storybook as a UI style guide to provide useful information to the design team
before they spend hours and hours creating many largely pointless âmockupsâ to show non technical stake holders what a
UI could look like đ.
Itâs here where I like to use decorators
.
Decorators can be used to render things but amazingly keep the âShow codeâ snippet super clean and developer friendly.
decorators
can be used to render components in a way that to non technical people may reflect how a page or feature
may look, but still provide a way for technical people to skip over what theyâre seeing and understand how to use whatâs
underneath.
⊠iâll explain myself.
Designers will often refer to their designs as âdesktopâ, âtabletâ and âmobileâ which isnât really a thing but I suppose the gist is this is we need to manage our applicationsâ view across a multitude of screen sizes.
For the most part this isnât the job of any individual âcomponentâ e.g the humble button. Instead its a job for âlayoutsâ or âgrid systemsâ.
This would be my preferred approach and to facilitate this upper level of layout management I try where possible to make lower level components âfluidâ. When I say âfluidâ I mean â100%â.
A simplified example of this might be:
// example a
<div style={{width: '500px'}}><MrButton>Click me</MrButton></div>
// example b
<div style={{width: '250px'}}><MrButton>Click me</MrButton></div>
In the two above examples iâve set a width
on the div
, example a = 500px and example b = 250px but in both cases
<MrButton />
will appear to fill the full width of the containing div. This is because <MrButton />
always fills
â100%â of the thing that itâs in.
In the context of a decorator this may manifest itself if youâre writing .stories
to demonstrate how two buttons can
be displayed side by side.
At the component level this isnât the job for <MrButton />
and he shouldnât know or care whatâs next to him and
shouldnât know or care about the space he needs to add in order to display correctly when placed next to
<MrsButton />
.
There are a number of ways to achieve this, but if you donât have to support IE11, iâd recommend using display: "grid"
To add decorators there are two main approaches. First is to apply a default decorator that will âwrapâ every story
in the .stories
file.
The below will add a div around all .stories
and apply a margin of 3em
. This approach works well if you want to
apply the same decorator to everything, in some cases this can be helpful but personally I prefer to apply the
decorators on a story-by-story basis, more on that in a moment.
default decorators
// components/mr-button/mr-button.stories.js
import React from "react"
import { MrButton } from "./mr-button"
export default {
title: "components/MrButton",
parameters: {
component: MrButton,
componentSubtitle: "The MrButton component is of html type button",
},
+ decorators: [
+ (Story) => (
+ <div style={{ margin: '3em' }}>
+ <Story />
+ </div>
+ )
+ ]
}
export const Usage = () => <MrButton>Mr Button</MrButton>
default args
While weâre talking about âdefaultsâ you can provide default args to every story using the same approach, in the case of
<MrButton />
You might find it helpful to apply a default arg to expose a handleClick
method that uses action
from
@storybook/addon-actions
which will âlogâ out button clicks to the actions panel. The actions panel is only visible
when youâre on the âcanvasâ tab âïž
// components/mr-button/mr-button.stories.js
import React from 'react';
+ import { action } from '@storybook/addon-actions';
import { MrButton } from './mr-button';
export default {
title: 'components/MrButton',
parameters: {
component: MrButton,
componentSubtitle:
'MrButton is a JavaScript component and is of html type button'
},
+ args: {
+ handleClick: (event) => action('handleClick')(event)
+ }
};
- export const Usage = () => (
+ export const Usage = (args) => (
- <MrButton>
+ <MrButton onClick={(event) => args.handleClick(event.currentTarget)}>
Click me
</MrButton>
);
The use of defaults may or may not suite your needs, in a lot of cases my preference is to apply decorators and or args
on a story-by-story basis. I appreciate this may result in some duplication but .stories
arenât youâre application or
component library code, theyâre documentation for humans and iâm of the opinion that if it makes the developer
experience better then thatâs more important than observing âbestâ coding practices that apply to application
development.
story decorators
As mentioned above you might have a requirement to show how two buttons can be displayed side by side. To work around
the Adjacent JSX elements must be wrapped in an enclosing tag warning I tend to wrap my components in React.Fragment
which could look like one of the following depending on your preference. Personally I prefer to use option 3 because it
keeps the âShow codeâ snippet looking squeaky clean.
<React.Fragment>...</React.Fragment>
<Fragment>...</Fragment>
<>...</>
Youâll see in LayoutStory.decorators
below that Iâm using a div
with some light inline styles to set itâs display as
grid plus a few other things to produce the layout I need, in this case gridTemplateColumns: '1fr 1fr',
works a treat
and combining it with gridGap: '16px',
means I have a nice space between the buttons.
// components/mr-button/mr-button.stories.js
...
export const LayoutStory = () => (
<>
<MrButton>Click me</MrButton>
<MrsButton isBoogyTime={true}>Also click me</MrsButton>
</>
);
LayoutStory.parameters = {
docs: {
description: {
story: '...'
}
}
};
LayoutStory.decorators = [
(Story) => (
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gridGap: '16px'
}}
>
<Story />
</div>
)
];
This would result in a nice looking âShow codeâ snippet that looks a little something like the below đ
// "Show code"
() => (
<>
<MrButton>Click me</MrButton>
<MrsButton isBoogyTime={true}>Also click me</MrsButton>
</>
);
Hopefully youâll agree with me and see the benefit of hiding the decorator <div>
so that the âShow codeâ snippet
only shows the buttons. But lets face it, we know we can use CSS to put things side by side, we donât need to provide
examples of how to do this in our button .stories
story args
Using the same approach as above with decorators
you can also apply args
on a story-by-story basis like the below đ
If, as mentioned above youâve already applied args
using the default approach the story-by-story approach will win out
export const ArgsStory = (args) => <MrButton onClick={() => args.handleClick('đș')}>Click Me</MrButton>;
ArgsStory.parameters = {
docs: {
description: {
story: '...',
},
},
};
ArgsStory.args = {
handleClick: (emoji) => action('handleClick')(emoji),
};
⊠finally Template.bind({})
There are a number of useful examples of how to use Template.bind({})
and it this approach has been covered really,
really well in the docs but I have run into a number
of issues when attempting to combine my above approach with Template.bind({})
I wonât cover that in this post but iâm sure if you follow the above and then add the following youâll see what I mean.
const Template = (args) => <MrButton {...args}>Click Me</MrButton>;
export const TemplateStory = Template.bind({});
TemplateStory.parameters = {
docs: {
description: {
story: '...',
},
},
};
TemplateStory.decorators = [
(Story) => (
<div style={{ margin: '3em' }}>
<Story />
</div>
),
];
TemplateStory.args = { ['aria-label']: 'A button that mostly does nothing' };
⊠which results in a âShow codeâ snippet that looks like this đ
// "Show code"
<div
style={{
margin: '3em',
}}
>
<No Display Name />
</div>
I think this comes back to one issue, and perhaps iâm alone in this way of thinking butâŠ
Template.bind({})
is trying to be too clever
For me, writing production JavaScript and taking advantage of ES6 (and beyond) shorthand methods are great for when youâre writing JavaScript applications.
This mindset is perhaps not so great when writing documentation.
Iâve worked on a few projects where Designers have asked if they could get involved with writing .stories
which is
wonderful, but imagine the problems I faced when I tried to explain what
Spread syntax was, that
and Designers donât care about duplicating code, they donât care about duplicating anything, they just want things
to look nice and âmatchâ their designs and for me lowering the barrier to entry to facilitate writing better more
informative .stories
is ultimately what Storybook is for.
Itâs for this reason that perhaps in some instances we, as Developers (or Engineers if youâre American) shouldnât be overly worried about being too clever or trying to write fewer lines of code.
My final word on the matter is:
Storybook is not your application, itâs your documentation⊠maybe different rules apply đ€·ââïž
That said, you are of course free to make up your own mind but I hope some of the above will demystify some of the
approaches you could take when writing .stories
⊠oh also, Iâm totally prepared to be proven wrong, many of my approaches are influenced by previous versions of
Storybook, and prior to v6.0.0
writing .stories
could at times be a little more âedgyâ
Come find me on Twitter if youâd like to discuss this more, iâm fully open and up for discussing this further đ @PaulieScanlon
Thanks for reading!
Cheerio