Thought leadership from the most innovative tech companies, all in one place.

A Guide to Documenting Controlled Components with Storybook

A Guide to Managing External Storybook State


My Journey

Storybook is a fantastic component explorer and playground. I use it develop, demonstrate, and document all my components across my codebases. Users can learn how a component works and how it is controlled. But, while setting up stories for stateless components such as buttons, grids, and tables were simple, components that required external state, such as form inputs, required more configuration.


A bit of background first. Let’s take this Input component

const Input = props => {
  const { className, onChange, type, value } = props;
  return (
      onChange={e => onChange(}

The stories written for this component would look like this:

import Input from './Input';

export default {
  title: 'Input',
  component: Input,

const Template = args => <Input {...args} />

export const Default = Template.bind({});

export const Number = Template.bind({});
Number.args = {
  type: 'number',

export const Password = Template.bind({});
Password.args = {
  type: 'password',

export const Value = Template.bind({});
Value.args = {
  value: 'value',

Template is an abstraction layer that reduces the amount of redundant code. Each named export creates a copy of this template and defines the props for the story through args. Storybook will take these and showcase the stories with the export name as the title.


Since this Input component requires you to provide onChange and value, we need to somehow manage this state using Storybook. But where? If you take a closer look, the template is itself a React functional component, and as such, we can use the useState hook to manage the input state!

const Template = args => {
  const [value, setValue] = useState(args.value ?? '');
  return (
        onChange={(...params) => {
      <pre style={{ marginTop: 10 }}>
        {JSON.stringify({ value }, null, 2)}
  • The initial state is set to args.value. This is so that we can have a story that defines the value to demonstrate how that prop works.

  • The onChange function defined for Input not only triggers the state hook updater, but also triggers args.onChange. The default Storybook setup automatically injects a Storybook Action Logger for all props that match the ^on[A-Z].* regex. The injected function will log every function call with its parameters in the actions tab so users can understand when and how it is triggered.

  • The pre tag displays the state value. While not completely necessary, this state value is essential to how the component works so rendering it helps the user understand the component better.

With this template change, our Input component now interacts as expected.


If you are using Storybook’s Controls Addon (which is automatically included with Storybook Essentials), the controls for the controlled value prop should also be disabled. Since the value is controlled via setState, nothing will happen if the prop value is changed in the controls tab.

export default {
  title: 'Input',
  component: Input,
  argTypes: {
    // controlled value prop
    value: {
      control: {
        disable: true,

Note, we don’t need to disable onChange since Storybook controls are automatically disabled for function props.

Final Thoughts

Storybook has tremendously benefited me and my team. It enables developers to showcase and document all the features of their component library all in one place. Using this simple trick of managing state in the story template, even controlled components can be documented in Storybook, enabling users to quickly learn the features and capabilities in a simple, yet power, interactive playground.


Continue Learning