Back to homepage

Storybook is an amazing tool for developing UI components in isolation. One of my current projects is a large form with controlled components that rely on their parent container as the source of truth. While Storybook is great for testing individual component state, I found myself writing repetitive code in each story to pass state to a parent container.

/* src/stories/index.js */
import React, { useState } from 'react';
import { storiesOf } from '@storybook/react';

storiesOf('Input', module).add('controlled', () => {
    function Parent({ children, ...props }) {
        const [state, setState] = useState();
        return <div>{children(state, setState)}</div>;
    }

    return (
        <Parent>
            {(state, setState) => (
                <input
                    value={state.value}
                    onChange={e => setState({ value: e.target.value })}
                />
            )}
        </Parent>
    );
});

This parent component could easily be abstracted and imported into relevant stories, but since each story is effectively a render function you would ideally pass the state variables through as arguments, i.e.

/* src/stories/index.js */
import React, { useState } from "react";
import { storiesOf } from "@storybook/react";

storiesOf("Input", module).add("controlled", (state, setState) => (
    <input
        value={state.value}
        onChange={e => setState({ value: e.target.value })}
    />
);

To solve this, I created two components and a custom decorator in the .storybook/config.js file. The first component is a function-as-child that acts as a render callback, emulating the parent component from my project. The second is a presentation component that receives state as a prop and displays the current value below each story. The custom decorator adds these components and state variables to each story, where the components wrap the story and the state values are passed as arguments.

/* .storybook/config.js */
import React, { useState } from "react";
import { configure, addDecorator } from "@storybook/react";

function loadStories() {
    require("../src/stories/index.js");
}

// Component 1
function Stage({ children, …props }) {
    const [state, setState] = useState({});
    return <div {…props}>{children(state, setState)}</div>;
}

// Component 2
function State({ state, …props }) {
    return (
        <div {…props}>
            Parent state: <pre>{JSON.stringify(state)}</pre>
        </div>
    );
}

// Custom decorator
addDecorator(story => (
    <Stage>
        {(state, setState) => (
            <div style={{ display: "flex", flexFlow: "column" }}>
                {story(state, setState)}
                <State state={state} />
            </div>
        )}
    </Stage>
));

configure(loadStories, module);

This allows each component to easily set and retrieve hoisted state values from the story itself, without any extra code. 💥

/* src/stories/index.js */
import React from 'react';
import { storiesOf } from '@storybook/react';

storiesOf('Input', module)
    // stateless
    .add('uncontrolled', () => <input />)

    // stateful
    .add('controlled', (state, setState) => (
        <input
            value={state.value}
            onChange={e => setState({ value: e.target.value })}
        />
));

You can find the repository on GitHub, and here’s a quick look at it in action:

1.1: Test both controlled and uncontrolled inputs in Storybook