Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot use React hooks directly inside a story #5721

Closed
sargant opened this issue Feb 22, 2019 · 56 comments
Closed

Cannot use React hooks directly inside a story #5721

sargant opened this issue Feb 22, 2019 · 56 comments

Comments

@sargant
Copy link

sargant commented Feb 22, 2019

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

@sargant sargant changed the title Cannot use hooks inside a story Cannot use React hooks inside a story Feb 22, 2019
@sargant sargant changed the title Cannot use React hooks inside a story Cannot use React hooks directly inside a story Feb 22, 2019
@gabefromutah
Copy link

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

@Keraito
Copy link
Contributor

Keraito commented Feb 24, 2019

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?

@ndelangen
Copy link
Member

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

@ndelangen
Copy link
Member

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

@sargant
Copy link
Author

sargant commented Feb 25, 2019

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

@Keraito
Copy link
Contributor

Keraito commented Feb 25, 2019

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.

@kevin940726
Copy link
Contributor

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

@shilman
Copy link
Member

shilman commented Mar 14, 2019

@kevin940726 that would be awesome 👍

@emjaksa
Copy link

emjaksa commented Mar 18, 2019

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

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

@paul-sachs
Copy link

paul-sachs commented Mar 18, 2019

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.

@kevin940726
Copy link
Contributor

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.

@orpheus
Copy link

orpheus commented Apr 1, 2019

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

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

@emjaksa Could you provide a snippet of this please?

@marcos-abreu
Copy link

marcos-abreu commented Apr 8, 2019

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)

@emjaksa
Copy link

emjaksa commented Apr 10, 2019

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)

@ghost
Copy link

ghost commented Apr 26, 2019

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

@dariye
Copy link

dariye commented May 4, 2019

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

@artyomtrityak
Copy link

artyomtrityak commented May 13, 2019

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

@shilman
Copy link
Member

shilman commented May 14, 2019

@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

@artyomtrityak
Copy link

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

@shilman
Copy link
Member

shilman commented May 14, 2019

@artyomtrityak I'll see what I can do 😆

@stale
Copy link

stale bot commented Jun 4, 2019

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!

@shiranZe
Copy link

shiranZe commented Aug 5, 2019

@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

@shilman
Copy link
Member

shilman commented Aug 5, 2019

@shiranZe are you using a recent 5.2-beta?

@shiranZe
Copy link

shiranZe commented Aug 5, 2019

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

@shilman
Copy link
Member

shilman commented Aug 5, 2019

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

@shiranZe
Copy link

shiranZe commented Aug 6, 2019

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.

@sourcesoft
Copy link

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.

@shilman
Copy link
Member

shilman commented Aug 16, 2019

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

@shilman
Copy link
Member

shilman commented Aug 16, 2019

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

@sourcesoft
Copy link

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

@zhenwenc
Copy link

zhenwenc commented Aug 16, 2019

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

@shilman
Copy link
Member

shilman commented Aug 17, 2019

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

@zhenwenc
Copy link

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

@dotexe0
Copy link

dotexe0 commented Aug 28, 2019

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

@StudioSpindle
Copy link

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

@Brandoncapecci
Copy link

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.

@GabLeRoux
Copy link

GabLeRoux commented Nov 26, 2019

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.

@GilCarvalhoDev
Copy link

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

@adeelibr
Copy link

how can this be achieved in MDX?

@shilman
Copy link
Member

shilman commented Feb 21, 2020

@adeelibr https://github.com/storybookjs/storybook/blob/17dcfb84194c9876d4452aae123443f9748f62b6/examples/official-storybook/stories/demo/button.stories.mdx#counter-w-code

@timiscoding
Copy link

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
#5721 (comment)

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.

@MahdiAbdi
Copy link

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

@claudio-basal-dev
Copy link

claudio-basal-dev commented Aug 7, 2020

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

@tmikeschu
Copy link

tmikeschu commented Dec 7, 2020


Does anyone know why this invoking decorator makes hooks work?

SomeComponent.decorators = [(Story) => <Story />]

@shilman
Copy link
Member

shilman commented Dec 7, 2020

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

@tmikeschu
Copy link

@shilman thank you!

@Tomekmularczyk
Copy link
Contributor

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

@albertoammar
Copy link

albertoammar commented Aug 7, 2021

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

@trajano
Copy link

trajano commented Oct 5, 2021

@albertoammar works well... Here's a more typed version of your solution as any is my trigger-warning. :D

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

export class Stories {
  stories: ReturnType<typeof storiesOfRN>;

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

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

    return this;
  }
}

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

This has a major flaw though and that is the "knobs" no longer work. I am continuing on https://stackoverflow.com/questions/65012604/storybook-react-hook since this thread is getting too long. and the topic is clsoed anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests