Category Archives: storybook

Getting Started Storybook (Latest update)

A while back I posted Getting started with Storybook. However, like with most things JavaScript, things have changed, and those instructions are now out of date and also, now, much simpler.

I have an existing React web app. so how do I add storybook ?

Note: Do not add storybook from NPM via yarn/npm, instead use the following instructions.

From you web app’s root folder run the following from the root of an existing project

npx storybook init

This will try to detect the framework being use. In the case of React this worked a treat. It will then add a stories folder to your src folder with a bunch of examples. It adds the .storybook folder with the information to tell storybook what files to look for and, ofcourse, add all the package.json dependencies etc.

Now all you need do is run

yarn storybook

So simple.

Here’s a very simple example of a .stories.tsx. I have a Header component which simpler writes a string out for a given date.

import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { Header } from '../controls/Header';

export default {
  title: 'Header',
  component: Header,
  parameters: {
    layout: 'fullscreen',
  },
} as ComponentMeta<typeof Header>;

const Template: ComponentStory<typeof Header> = (args) => <Header {...args} />;

export const Display = Template.bind({});
Display.args = {
   endDateTime: new Date(Date.parse("2022-12-31T23:59:00.000Z"))
};

The Display export is shown as a view on a component within storybook and we also get the endDateTime editor for trying different inputs out.

Storybook render decorator

Storybook have a bunch of really useful decorators.

This one is useful to see when changes, either data, state or properties cause re-rendering of our React UI. In some cases it may be that a change should not cause a subcomponent to render, with this decorator we can see what caused the render to occur (or at least help point us towards possible reasons).

We need to install the add-on, like this

yarn add -D storybook-addon-react-renders

and we just add code like this

import { withRenders } from "storybook-addon-react-renders";

storiesOf("Some Component Test", module)
  .addDecorator(withRenders)

Redux and storybook

We’ve implemented our React UI and set-up storybook to test it but we’re using redux as our store, so how do we use this in storybook?

Storybook allow us to create a decorator which is really just a wrapper around our story, so for example we add a decorator using .addDecorator like this

storiesOf("SomeComponent", module)
  .addDecorator(withProvider)
  .add("default", () => 
    <SomeComponent />
 );

Within the .addDecorator we can add more React code or HTML, maybe to position our test component centrally in the screen or in this case we can use the same code to wrap a Provider.

As you can see from the above code we’ve got a withProvider value which looks like this

const withProvider = (story) => <Provider store={store}>{story()}</Provider>

Hence the code takes a story (the story we’re testing) and we simply wrap it within a Provider element having previously created the redux store using the standard createStore function, i.e.

const store = createStore(reducer);

Now we can test our UI/UX code within storybook using the redux store for it’s state.

Storybook and state (also uses @sambego/storybook-state)

There’s several ways to setup your components for testing within Storybook to allow us to change our component’s properties – using Knobs or creating wrapper components to encompass state changes through to hooking up to a redux store.

Let’s look at an example of wrapping our component within a story.

So I have an AppBar within a selection component. I want to see the story maintain the state changes when the user changes the selection. Now, no actual state is stored within the component itself as it’s passed up the component hierarchy to a parent component and/or redux store. This means, without setting up a parent component or redux store the selection changes but the actual value on the component remains the same.

One way to achieve this is to create a wrapper parent component, i.e. a minimal wrapper component around our component under test, for example

class WrapperComponent extends React.Component<any, any> {
  constructor(props) {
    super(props);

    this.state = { ...props }
  }

  render() {
    const props = { 
      ...this.props,
      onSelectionChanged: (selected, _) => this.setState({selected: selected}),
      selected: this.state.selected
    }

    return <ComponentUnderTest {...props} />
  }
}

So as you can see, the ComponentUnderTest is wrapped in a WrapperComponent and hooked up to props along with state changes, now we simply have the story return the WrapperComponent, i.e.

const props = {
  selected: 'None'
}

return <WrapperComponent {...props}/>

Easy enough and could be rewritten to make it more generic, but there’s an alternative and that’s to use something like @sambego/storybook-state

yarn add -D @sambego/storybook-state

In which case we can write our story like this instead

import { Store, State } from "@sambego/storybook-state";

const storeState = new Store({
  selectedPublicQuery: "None"
});

storiesOf("ComponentUnderTest", module)
  .add("default", () => {
    return (
      <State store={storeState}>
        {state => 
          <ComponentUnderTest selected={state.selected} 
              onSelectionChanged={(selected, _) => storeState.set({selected: selected})}/>
          }
        </State>
      );
  });

Getting started with Storybook

Storybook allows us to prototype and test UI components in isolation from your application. We’re going to “get started” using Storybook with React, but it also supports Vue and Angular.

Let’s create the project and add required libraries.

Note, that the npx command should not be run from VSCode (or other Atom based editor by the sounds of it), I got an error EPERM: operation not permitted, unlink.

In the steps below we also install enzyme for test rendering. We’ll also add material-ui, just for the fun of it.

  • npx -p @storybook/cli sb init
  • yarn create react-app storybooksample –typescript
  • yarn add @types/storybook__react -D
  • yarn add @types/enzyme -D
  • yarn add enzyme -D
  • yarn add enzyme-adapter-react-16 -D
  • yarn add @types/enzyme-adapter-react-16 -D
  • yarn add @material-ui/core

The storybook “getting started” web page suggests we now run the following commands to check everything was installed correctly, so let’s do this, run

  • yarn test
  • yarn storybook

Let’s now remove the storybook generated code as we’ll replace this with our own code. So in the folder src/stories, delete index.js and add a file named setupTests.ts to the src folder, here’s the code for this file

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

Before we go too much further let’s create a sample component that will use to demonstrate testing via storybook. In the src folder add a folder named components/LogButton and in this create a file named LogButton.tsx – the name’s unimportant ofcourse, this is just a sample component which will be a new Button component, but hopefully will help demonstrate how we can use storybook.

In LogButton.tsx place the following code

import React from 'react';
import Button from '@material-ui/core/Button';

interface LogButtonProps {
    onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

export class LogButton extends React.Component<LogButtonProps, {}>{    
   render() {
      return (
         <div>
            <Button variant='contained' onClick={this.props.onClick}>Log</Button>
         </div>
      );
   }
}

Let’s now create a standard unit test for this component, this is not required for storyboard, but it’s good practise. In the same src/components/LogButton folder add the file LogButton.test.tsx and here’s a very simple test to got into this file

import React from 'react';
import { mount } from "enzyme";
import { LogButton } from "./LogButton";

test('Loading component should not throw error', () => {
    const logButton = mount(<LogButton />);
    expect(logButton.exists()).toBe(true);
});

This will just check that the component, when loaded, loads successfully. We can now run yarn test to verify this code works.

Now let’s get storybook up and running with our component.

In the .storybook folder, replace the config.js code with the following

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

const req = require.context('../src', true, /.stories.(ts|tsx|js)$/)
const loadStories = () => req.keys().forEach(filename => req(filename));
configure(loadStories, module)

This will automatically load stories based upon the filename/extensions .stories.

Alongside LogButton.tsx and LogButton.test.tsx, add LogButton.stories.tsx. This will supply the code required to integrate into storybook and also allow us to write code to allow us to test the UI component via storybook.

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

storiesOf('LogButton', module)
    .add("Log", () => <LogButton onClick={action('clicked')} />);

Now run yarn storybook and the storybook server and UI should display along with our LogButton now being the only component available. Selecting the “Log” story, when the LogButton is clicked the action will display the text “clicked”.

So what we’ve done is create a very simply component based upon a Button which we also declared some properties – well in this case we’ve created a single property, an onClick event. The story created for the button then hooks into the button’s click event and when used via storybook allows us to test the control in isolation via storybook’s UI. This is not a unit test or automated test so much as an interactive UI test where, as developers, we can verify our functionality. Let’s say the Button’s text changed when clicked, now we could start interacting with the button via storybook and confirm everything works as expected.

There’s a lot more to storyboard than the above, but this is a good starting point.