By Paul Scanlon

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

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

Hey!

Leave a reaction and let me know how I'm doing.

  • 0
  • 0
  • 0
  • 0
  • 0
Powered byNeon