Storybook: Cannot use React hooks directly inside a story

Created on 22 Feb 2019  ·  53Comments  ·  Source: storybookjs/storybook

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/[email protected]
[email protected]
[email protected]

react has workaround question / support

Most helpful comment

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 🙂.

All 53 comments

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 />);

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.

It shouldn't really matter AFAIK, but it's always worth trying. Also, @sargant what throws the error? Storybook or the type system?

@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.

@sargant does @gabefromutah's suggestion work for you?

@ndelangen @gabefromutah's suggestion is the same as my initial "this story works" example I believe?

In the case that this is not a bug, it might still be a useful enhancement to avoid having to use third-party plugins for state.

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

@ndelangen not entirely sure how that will interact with other certain addons, especially addon-info, which need information like props of the underlying rendered component.

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 🙂.

@kevin940726 that would be awesome 👍

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

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

I've implemented something similar, but I think that breaks a bunch of addons, specifically addon-info for outputting prop documentation. I've decided using hooks is more important than that info (as i generate that separately anyways) but I doubt that will apply to all of storybook in general. No idea how you would handle a hooks like api outside of react.

I tried to implement it in the source code, but I'm having a hard time to make it work with storyshot-addon. I think this would be a breaking change as it would generate a new parent node in every snapshots. As we are not in control of the renderer the user chooses, we cannot dive in one level deep. I think we might have to think of other alternatives, like we could document the solution and maybe provide some helper API for the users to opt-in.

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

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

@emjaksa Could you provide a snippet of this please?

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)

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)

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

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

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

@artyomtrityak You can override the propTables option in addon-info? I'll be solving this properly in the upcoming addon-docs: https://medium.com/storybookjs/storybook-docs-sneak-peak-5be78445094a

@shilman will it also include source of <DropdownBasicStory />?

@artyomtrityak I'll see what I can do 😆

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

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.

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>
));

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

Be careful with the above React.createElement and <Story /> approach, I think they are not quite right in some cases, because you are actually re-creating the react components (the story component) instead of returning the rendered elements (while using story() in the storybook config.js as shown in the docs).

For example, when your story has Knobs.button that triggers component local state updates, after clicking the button, you are likely creating a new story component instead of updating the state of the current component.

You could verify my assumption with this simple code snippet, where the value won't be updated after clicking the knobs button.

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>;
  });
});

To make it works, you can do:

function MyStory() {
  const [value, setValue] = React.useState(1);

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

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

storiesOf('Test', module).add('with text', () => <MyStory />);

Therefore, as a workaround, I have created a wrapper:

import {
  DecoratorParameters,
  Story,
  StoryDecorator,
  storiesOf as origStoriesOf,
} from '@storybook/react';

class ReactStory {
  private readonly story: Story;

  constructor(name: string, module: NodeModule) {
    this.story = origStoriesOf(name, module);
  }

  public add(
    storyName: string,
    Component: React.ComponentType,
    parameters?: DecoratorParameters
  ): this {
    this.story.add(storyName, () => <Component />, parameters);
    return this;
  }

  public addDecorator(decorator: StoryDecorator): this {
    this.story.addDecorator(decorator);
    return this;
  }

  public addParameters(parameters: DecoratorParameters): this {
    this.story.addParameters(parameters);
    return this;
  }
}

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

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

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

Optionally, you can mimic the storybook API to reduce the refactoring overhead, and switch back later when the issue had been fixed.

export function storiesOf(name: string, module: NodeModule) {
  return new ReactStory(name, module);
}

I might be wrong, if so, please let me know. Much appreciated!

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.

@shilman thanks!! do you have a code snippet example of how to use it?

@shilman thank you!
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

@shiranZe are you using a recent 5.2-beta?

@shilman yes...

my code on stories/components/Menu/index.js:

const component = () => {
const [buttonEl, setButtonEl] = useState(null)

const handleClick = (event) => {
    console.log('the event', event)
    setButtonEl(event.currentTarget)
}

return (
    <>
        <IconButton iconName={"menu-hamburger"} size="s" onClick={handleClick} color={"midGray"}></IconButton>

export default [readme, component];

and in the stories/components/index.js:
storiesOf('Components', module) .addDecorator(withKnobs) .add('Text', withReadme(...Menu))

@shiranZe i'd guess it's an issue with addon-readme -- maybe file an issue there?

Thank you @shilman for your reply. I don't know if that's the problem . I tried without addon-readme and still getting the same error. do you have a URL of 5.2-beta story book?
i tried to take a look at https://storybooks-official.netlify.com/ (other|demo/Button)
but i can't find there the example with react hooks.

I still have the issue with 5.2.0-beta.30 and the thing is none of the workarounds worked for me, since I'm using knobs addon too.

@shiranZe our netlify deploy is turned off (cc @ndelangen) but you can check out the repo on the next branch and try it there.

@sourcesoft I believe that the knobs-related "preview hooks" issue (cc @Hypnosphi) has nothing to do with this issue -- the only thing they have in common is the concept of hooks.

@shilman Thanks, I think you're right. Btw I figured out how to fix it by trial and error, changed the custom hook:

export const useField = (id, updateField) => {
  const onChange = useCallback((event) => {
    const {
      target: { value },
    } = e;
    updateField(id, value);
  };

  return {
    onChange,
  };
}, []);

to

export const useField = (id, updateField) => {
  const onChange = (event) => {
    const {
      target: { value },
    } = e;
    updateField(id, value);
  };

  return {
    onChange,
  };
};

Basically just removed the use of useCallback here. I'm not sure if the first version was a valid hook but it used to work. Also it's a bit confusing when the error says Hooks can only be called inside the body of a function component or having multiple version of React.

In your above example, after removing the useCallback, are you actually using registering any hook in useField at all? 🤔

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

@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.

@zhenwenc FYI the code works, but breaks usage of docs rendered by react-docgen-typescript-webpack-plugin.

Workaround seems a bit brittle and conflicting with other libraries. As an alternative, does someone have experience using?: https://github.com/Sambego/storybook-state

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.

Just answering @orpheus's request for a snippet

Adding the following to my .storybook/config.js worked for me
addDecorator((Story) => <Story />)

@emjaksa Could you provide a snippet of this please?

diff --git a/.storybook/config.js b/.storybook/config.js
--- a/.storybook/config.js
+++ b/.storybook/config.js
@@ -1,6 +1,9 @@
-import { configure } from '@storybook/react';
+import { configure, addDecorator } from '@storybook/react';
+import React from 'react';

 // automatically import all files ending in *.stories.js
 configure(require.context('../stories', true, /\.stories\.js$/), module);
+
+addDecorator((Story) => <Story />);

Result (complete .storybook/config.js):

import { configure, addDecorator } from '@storybook/react';
import React from 'react';

// automatically import all files ending in *.stories.js
configure(require.context('../stories', true, /\.stories\.js$/), module);

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

I'm not sure if this is the best way to get react hooks working inside storybook, but wrapping storybook in its own component <Story /> component worked here with "@storybook/react": "^5.2.6",.

Before doing this, whenever I updated a knob (ie: a boolean), it worked on first render, but then stopped rendering afterward. Above solution fixed that. Btw, I'm not sure it's good practice to use react hooks in storybook, just mock everything if possible, it's probably better this way.

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/[email protected]
[email protected]
[email protected]

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

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 })}
    />
  );
});

how can this be achieved in MDX?

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.

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

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

@emjaksa Thanx! Tested and working fine on Storybook v5.3.

I had the same issue, my functional react component has useState(value) and value didn't re-render the new value from Knobs, this issue is caused by useState:

From React docs:
Screen Shot 2020-08-07 at 19 00 04

Working solution Storybook v5.3:

Update preview.js

.storybook/preview.js
import React from 'react'; // Important to render the story
import { withKnobs } from '@storybook/addon-knobs';
import { addDecorator } from '@storybook/react';

addDecorator(withKnobs);
addDecorator(Story => <Story />); // This guy will re-render the story

And also update main.js

.storybook/main.js
module.exports = {
  stories: ['../src/**/*.stories.jsx'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-knobs/register', // Attention to this guy
    '@storybook/addon-actions',
    '@storybook/addon-links',
  ],
  webpackFinal: async config => {
    return config;
  },
};



Does anyone know why this invoking decorator makes hooks work?

```tsx
SomeComponent.decorators = [(Story) => ]

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

@shilman thank you!

Was this page helpful?
0 / 5 - 0 ratings