Adding StorybookJS to your application shouldn't be some month-long endeavor. Instead, you should add the basic configuration and setup now, with a few simple examples, and improve your codebase incrementally over time.
As you build any React application, you will undoubtedly get to the point where your application reuses components across multiple screens and views. In fact, if you're properly following atomic design, then you will probably get to this point almost immediately.
StorybookJS is the perfect tool for documenting and visually testing components in your codebase.
Sadly, many teams hold off on adding Storybook to their application for far too long because they think that they have to move their entire application over to Storybook at once.
Not only is this untrue, but it is an anti-goal. Why?
When adding any developer tooling, you should always do so incrementally.
So, in order to get started and enable our teams as quickly as possible we should spend a minimal amount of time setting it up, with the most simplistic examples so that you can learn and develop best practices over time.
To begin using Storybook today, add your simplest components (your "atoms") and go form there.
This means following the getting started guide. TL;DR use npx sb init
to add storybook boilerplate config to your repo.
This will probably require some finagling on your end if you use a custom webpack, etc, but the general set-up should still be quite simple.
These should be the absolute simplest components. You can migrate a few if you have time, but since I was our team was in crunch mode trying to deliver on a new product, I focussed on adding just a single example.
Some suggestions: Typography and Buttons are effectively the easiest. No matter what you're working on, I'm sure you have some way of sharing typography styles across your application (globally, styled-components, etc) just treat this as nothing more than the easiest way to document your headings (h1–h6) and various text elements however you would actually use them in your application.
// Typography.stories.js
import { H1, H2, H3, H4, H5, H6 } from './Typography';
export default {
title: 'Section/Component',
component: Component
}
export const header1 = () => <H1>Example Text</H1>
export const header2 = () => <H2>Example Text</H2>
export const header3 = () => <H3>Example Text</H3>
export const header4 = () => <H4>Example Text</H4>
export const header5 = () => <H5>Example Text</H5>
export const header6 = () => <H6>Example Text</H6>
Congratulations! You now have a Storybook design system for your application.
Whenever you are updating the design of a component, it is good practice to add a story to Storybook for it. In 80% of cases, it takes just a few minutes to add a *.stories.js
// Component.stories.js
import Component from './Component';
export default {
title: 'Section/Component',
component: Component
}
export const example = () => <Component />
If a story has any complications, like dependency on a context you haven't had to mock yet, or some other mysterious issue (they do occasionally crop up) leave it to your best judgement to making it work, versus completing your work. If it's just some small content change on some page, then it's probably not worth the distraction.
You have two options:
This is often the best approach, because these sort of outside dependencies are often done unintentionally, because they were not being built with re-use or modularity in mind. The time spent refactoring could make it easier to demonstrate hard-to-access states in components, and will effectively serve as a set of visual unit tests for your components.
This could either be a mock, or even an actual instance of the dependency. This means you will be displaying the components similarly to how they are used in the application, and will likely be the easiest way to get it working
For example, let's you are working on a React/Redux application and want to add a page component that contains nested connected components, and perhaps some React Router links. This would require you to include a Redux context and React Router context:
// Page.stories.js
import { Provider } from 'react-redux';
import Store from '../store';
import Page from './Page';
export default {
title: 'Section/Component',
component: Component,
decorators: [
Story => (
<MemoryRouter initialEntries={['/']}>
<Story {...context} />
</MemoryRouter>
),
Story => (
<Provider store={store}>
<Story {...context} />
</Provider>
)
]
}
export const example = () => <Page />
You'll notice that I've done this with decorators, which is a pretty clean way to handle across multiple stories, or to add multiple providers.
To display different states, you can dispatch simple events yourself:
// Header.stories.js
import { MemoryRouter } from 'react-router';
import { Provider, useDispatch } from 'react-redux';
import Store from '../store';
import { userLoginSuccess, userLogout } from '../../store/actions';
import Header from './Header';
export default {
title: 'Section/Component',
component: Component,
decorators: [
Story => (
<MemoryRouter initialEntries={['/']}>
<Story {...context} />
</MemoryRouter>
),
Story => (
<Provider store={store}>
<Story />
</Provider>
)
]
}
export const header = (args) => {
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(userLogout())
})
return (
<Header {...args} />
)
}
header.args = { }
export const headerWithLogin = (args) => {
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(userLoginSuccess(adaptResponseToUser(args.userResponse)))
})
return (
<Header {...args} />
)
}
headerWithLogin.args = {
userProfile: {
firstName: 'John',
lastName: 'Doe'
}
}
You'll notice that I had to dispatch a logout action here. This is because, in this case, my stories each share the same global instance of store. However, if we were to take this a step further and mock our store, then we could create a reset method. In this case, I didn't have the time or need for a mock, and I want to keep things simple, so I simply used my existing store.
Remember, we're trying to be pragmatic here. We just want a single pristine example of how our component is used.