Storybook - An alternative approach

Date published: 27-Oct-2020
11 min read / 2903 words
Author: Paul Scanlon

Storybook
JavaScript
React

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.

  1. <React.Fragment>...</React.Fragment>
  2. <Fragment>...</Fragment>
  3. <>...</>

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



If you've enjoyed this post I'd love to hear from you: @PaulieScanlon