Storybook
A UI development tool to develop and test components isolated.
Installation
To install Storybook you will only need to run:
npm i --save-dev @storybook/react @types/storybook__react awesome-typescript-loader react-docgen-typescript-webpack-plugin
See their official web page for more.
Plugins
In Storybook we have something called addons.
Recommended plugins
actions: can be used to display data received by event handlers.
info: will show additional information for stories.
knobs: allow you to edit React props dynamically using the Storybook UI.
jsx: shows the JSX of the component.
All of them can be installed by running:
npm i --save-dev @storybook/addon-actions @types/storybook__addon-actions
npm i --save-dev @storybook/addon-info @types/storybook__addon-info
npm i --save-dev @storybook/addon-knobs @types/storybook__addon-knobs
npm i --save-dev storybook-addon-jsx
Configuration
We will proceed to create a config folder only for Storybook in the root of our project. It will look like this:
.storybook
├── addons.js
├── config.js
├── style.js
└── webpack.config.js
addons.js file will import all the addons we need for Storybook:
import "@storybook/addon-options/register";
import "@storybook/addon-knobs/register";
import "@storybook/addon-actions/register";
import 'storybook-addon-jsx/register';
The config.js file will contain all the configuration. Is the one responsible to load all the stories:
import { configure, addDecorator, setAddon } from "@storybook/react";
import "../src/styles.css";
import { setOptions } from "@storybook/addon-options";
import { withKnobs } from "@storybook/addon-knobs";
import JSXAddon from 'storybook-addon-jsx';
setAddon(JSXAddon);
// Load all the stories
const req = require.context("../src/components", true, /\.story\.tsx$/);
function loadStories() {
req.keys().forEach(filename => req(filename));
}
// Default decorators
addDecorator(withKnobs);
// Addon-options configuration
setOptions({
name: "shark-events-client",
addonPanelInRight: false,
sortStoriesByKind: true,
sidebarAnimations: false
});
configure(loadStories, module);
Finally, we will need to set our webpack.config.js file. Because we are using TypeScript, our info addon will need to import a plugin called react-docgen-typescript-webpack-plugin:
const path = require("path");
const TSDocgenPlugin = require("react-docgen-typescript-webpack-plugin");
module.exports = (baseConfig, env, config) => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
loader: require.resolve("awesome-typescript-loader")
});
config.plugins.push(new TSDocgenPlugin()); // optional
config.resolve.extensions.push(".ts", ".tsx");
return config;
};
JSX addon does not have types. TypeScript will complain once you add it to a component.
To solve this, create a new dile inside your project in the following path src/storybook.d.ts:
import { RenderFunction } from "@storybook/react";
declare module "@storybook/react" {
export interface Story {
addWithJSX(storyName: string, callback: RenderFunction): this;
}
}
Component example
In our application we have a button component:
import { boolean, select, text } from "@storybook/addon-knobs";
import { storiesOf } from "@storybook/react";
import React from "react";
import { EditIcon } from "../icon";
import { APPEARANCE_DEFAULT } from "../lozenge/lozenge.constant";
import { Button } from "./button";
import { BUTTON_APPEARANCE_DEFAULT, BUTTON_APPEARANCE_PRIMARY, BUTTON_APPEARANCE_TRANSPARENT } from "./button.constant";
const appearanceOptions = {
default: BUTTON_APPEARANCE_DEFAULT,
transparent: BUTTON_APPEARANCE_TRANSPARENT,
primary: BUTTON_APPEARANCE_PRIMARY
};
storiesOf("Button", module).addWithJSX("as dynamic variables", () => {
const placeholder = text("Text", "Lorem");
const appearance = select("Appearance", appearanceOptions, APPEARANCE_DEFAULT);
const isDisabled = boolean("Is disabled", false);
const isLoading = boolean("Is loading", false);
const hasIcon = boolean("Has icon", true);
const isIconBefore = boolean("Is icon before", true);
const icon = hasIcon ? <EditIcon width={18} height={18} /> : undefined;
const content = (
<Button
text={placeholder}
appearance={appearance}
icon={icon}
isIconBefore={isIconBefore}
isDisabled={isDisabled}
isLoading={isLoading}
/>
);
return <div>{content}</div>;
});
We don't need to add a decorator here for knobs because we already set this as default in our config.js file:
addDecorator(withKnobs);
However, if we want to use the JSX addon, we will need to add a decorator in our story called addWithJSX:
storiesOf("Button", module).addWithJSX("story", () => <p>Lorem</p>))
Run Storybook
We can add a command in our package.json:
"storybook": "start-storybook -p 9001 -c .storybook"