storybook: Cannot use React hooks directly inside a story

Describe the bug

Cannot use hooks directly in a story, fails with Hooks can only be called inside the body of a function component.

To Reproduce

Example code:

import React from 'react'

import { storiesOf } from '@storybook/react'

const stories = storiesOf('Hooks test', module)

const TestComponent: React.FC = () => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
}

stories.add('this story works', () => <TestComponent />)

stories.add('this story fails', () => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
})

Expected behavior First story works OK, second story fails on initial render

Versions @storybook/react@4.1.13 react@16.8.2 react-dom@16.8.2

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 54
  • Comments: 56 (20 by maintainers)

Commits related to this issue

Most upvoted comments

We’ve experienced the same issue, and this is a simple hack we do to workaround it.

stories.add('this story fails', () => React.createElement(() => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
}))

I agree it would be much better for it to be natively supported without hacks. If the maintainers agree too then maybe I can find some time to come up with a PR 🙂.

Adding the following to my .storybook/config.js worked for me

addDecorator((Story) => <Story />)

For those here googling the error message:

I got this error when a changing a class component to a functional component with hooks and useState was incorrectly importing from @storybook/addons. I needed it to come from react rather than @storybook/addons… auto-import fail.

Not sure this is a bug. I believe, and I could be wrong, the second argument of stories.add is expecting a function to return a component and not an actual React component. Try moving your function component to the outside, you should have some success…

Example

function SomeComponent() {
	const [blah] = React.useState('blah');
	return <div> {blah}</div>;
}
stories.add('BlahComponent', () => <SomeComponent />);

My info addon doesn’t break provided I added the Story decorator last.

import React from 'react'

import { configure, addDecorator } from '@storybook/react'
import { withInfo } from '@storybook/addon-info'
import { withKnobs } from '@storybook/addon-knobs'

const req = require.context('../src', true, /\.stories\.js$/)

function loadStories() {
  req.keys().forEach(filename => req(filename))
}

addDecorator(
  withInfo({
    header: false,
  }),
)
addDecorator(withKnobs)
addDecorator((Story) => (
    <Story />
))

configure(loadStories, module)

Ta-da!! I just released https://github.com/storybookjs/storybook/releases/tag/v5.2.0-beta.10 containing PR #7571 that references this issue. Upgrade today to try it out!

You can find this prerelease on the @next NPM tag.

Closing this issue. Please re-open if you think there’s still more to do.

@tmikeschu it’s because the <Story /> wraps the code in React.createElement which is necessary for hooks.

but still getting the error: React Hook “useState” is called in function “component” which is neither a React function component or a custom React Hook function https://github.com/storybookjs/storybook/issues/5721#issuecomment-518225880

I was having this exact same issue. The fix is to capitalize the named story export.

 
import React from 'react';
import Foo from './Foo';

export default {
  title: 'Foo';
};

export const Basic = () => <Foo />

The docs say capitalization is recommended but it’s necessary if you want that warning to go away.

In my case, the problem was that I forgot to import the hook I was using.

Describe the bug

Cannot use hooks directly in a story, fails with Hooks can only be called inside the body of a function component.

To Reproduce

Example code:

import React from 'react'

import { storiesOf } from '@storybook/react'

const stories = storiesOf('Hooks test', module)

const TestComponent: React.FC = () => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
}

stories.add('this story works', () => <TestComponent />)

stories.add('this story fails', () => {
  const [state, setState] = React.useState(5)
  return (
    <button onClick={() => setState(state + 1)}>
      {state}
    </button>
  )
})

Expected behavior First story works OK, second story fails on initial render

Versions @storybook/react@4.1.13 react@16.8.2 react-dom@16.8.2

======================================

Do not use arrow function to create functional components. Do as one of the examples below:

function MyComponent(props) {
  const [states, setStates] = React.useState({ value: '' });

  return (
    <input
      type="text"
      value={states.value}
      onChange={(event) => setStates({ value: event.target.value })}
    />
  );
}

Or

//IMPORTANT: Repeat the function name

const MyComponent = function MyComponent(props) { 
  const [states, setStates] = React.useState({ value: '' });

  return (
    <input
      type="text"
      value={states.value}
      onChange={(event) => setStates({ value: event.target.value })}
    />
  );
};

If you have problems with “ref” (probably in loops), the solution is to use forwardRef():

// IMPORTANT: Repeat the function name
// Add the "ref" argument to the function, in case you need to use it.

const MyComponent = React.forwardRef( function MyComponent(props, ref) {
  const [states, setStates] = React.useState({ value: '' });

  return (
    <input
      type="text"
      value={states.value}
      onChange={(event) => setStates({ value: event.target.value })}
    />
  );
});

Yet another workaround:

I have a utility component called UseState defined like this:

export const UseState = ({ render, initialValue }) => {
    const [ variable, setVariable ] = useState(initialValue)
    return render(variable, setVariable)
}

And I use it in the stories like this:

.add('use state example', () => (
    <UseState
        initialValue={0}
        render={(counter, setCounter) => (            
            <button onClick={() => setCounter(counter + 1)} >Clicked {counter} times</button>
        )}
    />
)

But I like @kevin940726 's workaround the best

Also watch out for IDE autoimport feature, instead of importing useState from:

import { useState } from 'react';

VSC imported it from:

import { useState } from '@storybook/addons';

I’m unable to get the story to re-render on state change. Force re-rendering doesn’t work either.

@Keraito No I’m pretty sure the error is correct.

It’s because we’re calling the function out of the context of react, aka storybook will call the function like so:

const element = storyFn();

not

const element = <StoryFn />

Quite possibly if we’d initiate it like that it might work.

Right now @gabefromutah’s advice is sound.

Here’s the actual line of code: https://github.com/storybooks/storybook/blob/next/app/react/src/client/preview/render.js#L24

If someone wants to experiment to make this work, that’s the place to start, I think.

how can this be achieved in MDX?

it sounds like there’s some issue with using hooks with knobs. If anybody can provide a simple repro, I’d be happy to take a look at it

To display the source code of components with hooks

function WithState({ children }) {
  const [value, setValue] = React.useState([]);

  return React.cloneElement(children, {
    value,
    onChange: event => setValue(event.target.value),
  });
}

storiesOf(`${__dirname}`, module).add('Basic', () => (
  <WithState>
    <select value="[parent state]" onChange="[parent func]">
      <option value="Australia">Australia</option>
      <option value="Cambodia">Cambodia</option>
    </select>
  </WithState>
));

@shilman Have you tried the example I posted earlier? Here is the snippet:

storiesOf('Test', module).add('with text', () => {
  return React.createElement(() => {
    const [value, setValue] = React.useState(1);

    Knobs.button('Increase', () => setValue(prev => prev + 1));

    return <span>{value}</span>;
  });
});

Its using the old API, but should be easy to transform to the latest API for testing. You could find the expected behaviour from the original post.

While this code works well for React Hooks

storiesOf("Dropdowns", module).add("Basic", () => <DropdownBasicStory />);

It does not work well with @storybook/addon-info: Screenshot 2019-05-13 14 35 10

This makes this workaround unusable. Any ideas? Storybook 5.1.0-beta.0

This was my workaround to this problem:

import React, { useState } from 'react';

import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withInfo } from '@storybook/addon-info';

import SelectField from 'component-folder/SelectField';

/**
 * special wrapper that replaces the `value` and `onChange` properties to make
 * the component work hooks
 */
const SelectFieldWrapper = props => {
  const [selectValue, setValue] = useState('');

  return (
    <SelectField
      {...props}
      value={selectValue}
      onChange={e => {
        setValue(e.target.value);
        action('onChange')(e.target.value);
      }}
    />
  );
};
SelectFieldWrapper.displayName = 'SelectField';

const info = {
  text: SelectField.__docgenInfo.description,
  propTables: [SelectField],
  propTablesExclude: [SelectFieldWrapper]
};

storiesOf('Controls/SelectField', module)
  .addDecorator(withInfo)

  // ... some stories

  // this example uses a wrapper component to handle the `value` and `onChange` props, but it should
  // be interpreted as a <SelectField> component
  .add('change handler', () => 
    <SelectFieldWrapper
      id="employment-status"
      placeholder="some placeholder"
      value={//selectValue}
      onChange={e => {
          // setValue(e.target.value);
      }}
    />, { info });

As I mentioned is still a workaround, but it does work and don’t break the info addon. (I haven’t tested other addons)

@kevin940726 that would be awesome 👍

I created a simple class to work with to fix this:

import React from 'react';
import { storiesOf as storiesOfRN } from '@storybook/react-native';

export class Stories {
   storyName: string;

   stories: any;

   constructor(storyName: string) {
      this.stories = storiesOfRN(storyName, module);
   }

   add(name: string, fn: any): this {
      const Fn = React.memo(fn);
      this.stories.add(name, () => <Fn />);

      return this;
   }
}

export function storiesOf(storyName, b): Stories {
   return new Stories(storyName);
}

and use:

import React from 'react';
import { storiesOf } from '../../../Stories';

storiesOf('Component', module)
   .add('default', () => {
      const [state, setOpenModal] = useState(true);

       return null;
   });

If anyone else is still getting this, i found that the issue seems to be adding prop types to a functional component.

const Dropdown = () => (
  // component content
);

Dropdown.propTypes = {
  ...
}

For some reason commenting out .propTypes seems to make it work. Not sure if it’s an issue with the prop types parsing for documentation or something else.