Storybook: Storybook not compatible with React hooks

Created on 2 Nov 2018  ยท  73Comments  ยท  Source: storybookjs/storybook

Describe the bug
Attempting to render a react component that uses hooks into a storybook staging environment throws an error Hooks can only be called inside the body of a function component.

To Reproduce
Steps to reproduce the behavior:

  1. Create a react component that uses hooks
  2. Import & render the component in storybook

Expected behavior
Storybook should be able to display React components that use hooks.

Code snippets

Component code

import React, { useState } from "react";

export default function ColorChanger() {
  const [color, setColor] = useState("#000");
  const randomColor = "#" + Math.floor(Math.random() * 16777215).toString(16);
  return (
    <div style={{ color }} onClick={() => setColor(randomColor)}>
      Color is: {color} (click to change)
    </div>
  );
}

Note, this code is working on codesandbox: https://codesandbox.io/s/n5rmo77jx0

System:

  • Browser: Firefox
  • Framework: React
  • Version: 4.0.2
react question / support

Most helpful comment

So I ran into this issue using CRA 2.1.1 with typescript on the 4.1.0-alpha.8 version of @storybook/react and hooks and it ended up coming down to the fact that the package includes it's own local copy of React so when the hook is called it can't find the ReactSharedInternals.ReactCurrentOwner.currentDispatcher instance.
As silly as it felt, I just updated my npm scripts to the following:

"storybook": "rm -rf ./node_modules/@storybook/react/node_modules/react && rm -rf ./node_modules/@storybook/react/node_modules/react-dom && start-storybook -p 9009",

And everything seemed to work. Maybe react/react-dom need to be peer-dependencies of @storybook/react instead of regular deps?

I tried the react-hot-loader setConfig thing only to find that react-hot-loader wasn't installed in the first place. Maybe this is a change with the 4.1 alpha but I'm new to storybook in general so I wouldn't know.

All 73 comments

Any suggestion I am getting the same error?

I had the same problem but got it working.

  • First I had an error because my function wasn't capitalized e.g. example => Example
  • Second I forgot how to React and tried to call the function, instead of passing it as JSX element

    • So to use ilias-t's example, render the function component like <ColorChanger /> and not ColorChanger()

  • Lastly, after rendering my function as JSX element, it said I needed to import React in my example code from the React docs.

    • And now it works.

I did also update the react-dom package to 16.7.0-alpha.0 before I tried any of this, so perhaps, if you haven't already, give that a go as well.

@Amolang Pretty sure I'm doing all of the same steps. The specific error message that I'm seeing leads me to believe the error isn't a result of one of those cases.

Good thought regarding react-domโ€”I thought that might be it, but I updated it and still go the same error ๐Ÿ˜•

@Amolang I am doing all the same thing as you explained and getting the same error as @ilias-t suggested.
Further more investigation land me to resolveDispatcher() function in react library where its calling
ReactCurrentOwner.currentDispatcher;
and which is set like below on top somewhere in the react.development.js file

var ReactCurrentOwner = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: null,
  currentDispatcher: null
};

and thats why I am getting Hooks can only be called inside the body of a function component
as in the code below

function resolveDispatcher() {
  var dispatcher = ReactCurrentOwner.currentDispatcher;
  !(dispatcher !== null) ? invariant(false, 'Hooks can only be called inside the body of a function component.') : void 0;
  return dispatcher;
}

Might be a silly question but, is both of your React packages also updated to 16.7.0-alpha.0?

That is the only other thing I have done, which I didn't mention above.

Yes. It looks like it may have something to do with this: https://github.com/facebook/react/issues/13972#issuecomment-433538714, which is in turn related to https://github.com/facebook/react/issues/14022

It doesn't look like storybook uses ReactDOMServer, but it may be trying to do custom react-tree traversal, like @theKashey mentioned.

I think you will have more luck finding answers in those issues. My knowledge is very limited.

@Amolang No worries! Would you mind uploading the project that you got it working in, so I can compare? It definitely could be an issue with some of my configs. I was trying to do it in a package of a lerna'ed repo, which adds a layer of complexity ๐Ÿ˜ฌ

The code can be found in my project.
The Example is in App.js under src/client. :)

@Amolang am I missing something? Doesn't look like you have storybook installed in that project ๐Ÿ˜•

I don't. I found this issue by Google search, hoped to find an answer.

I then tried what I mentioned above and got it working. Thought it would work for both of you as well.

But based on the issues you linked, it seems to me like it happens on server render, which I don't use.

@Amolang ๐Ÿ˜„ gotcha, yeah this issue is only happening for me in when using hooks with storybook, which is why I filed the issue for storybook specifically. Will update the title to make it more clear that it's storybook specific.

Try:

import { setConfig } from 'react-hot-loader';
setConfig({ pureSFC: true });

in your .config file.

This should do the trick - for now.

@phaistonian Tried this and still this
image

Thanks @phaistonian that totally worked!

import { setConfig } from 'react-hot-loader';
setConfig({ pureSFC: true });

This didn't work for me, I get the exact same error with or without it.

Could anyone provide a bit more verbose output?
pureSFC should work, and it's easy to check it - _hooked_ should be called from a function named ProxyFacade, not hotComponentRender

@theKashey How do you check which function is calling _hooked_?

Nvm, I was using hooks inside of the .add('with ...', () => { ... return (<Component></Component>) } part which is a no go, my bad. Instead I'm creating a function outside of it and using it like .add('with ...', () => (<ComponentWithHooks></ComponentWithHooks>)) to test it with hooks.

And it works?

@theKashey yeah it's working fine with the setConfig({ pureSFC: true }); line.

Please try ReactHotLoader@next.
It potentially could solve any problems, but would require react-dom to be processed by RHL webpack-loader. Ie could cause problems with custom webpack configuration, and will not work for the configuration from CRA.

@theKashey Is there a way to get around this issue when using a custom webpack configuration?

Just use webpack aliases to rewire react-dom to hot-loader/react-dom - see this thread - https://github.com/gaearon/react-hot-loader/issues/1088#issuecomment-440624088

So I ran into this issue using CRA 2.1.1 with typescript on the 4.1.0-alpha.8 version of @storybook/react and hooks and it ended up coming down to the fact that the package includes it's own local copy of React so when the hook is called it can't find the ReactSharedInternals.ReactCurrentOwner.currentDispatcher instance.
As silly as it felt, I just updated my npm scripts to the following:

"storybook": "rm -rf ./node_modules/@storybook/react/node_modules/react && rm -rf ./node_modules/@storybook/react/node_modules/react-dom && start-storybook -p 9009",

And everything seemed to work. Maybe react/react-dom need to be peer-dependencies of @storybook/react instead of regular deps?

I tried the react-hot-loader setConfig thing only to find that react-hot-loader wasn't installed in the first place. Maybe this is a change with the 4.1 alpha but I'm new to storybook in general so I wouldn't know.

That was done in order to support different versions of React. One for the manager and one for the client. Is there any way we can make hooks available out of the box ๐Ÿค” ?

Hello,

I tried the last version of react-hot-loader (4.5.2) and I'm still unable to make storybook works with it.

I have this error :

Hooks can only be called inside the body of a function component.
    Invariant Violation: Hooks can only be called inside the body of a function component.
    at invariant (http://localhost:9009/vendors~main.fefa65282a203b643a70.bundle.js:562315:15)
    at resolveDispatcher (http://localhost:9009/vendors~main.fefa65282a203b643a70.bundle.js:563614:28)
    at useState (http://localhost:9009/vendors~main.fefa65282a203b643a70.bundle.js:563637:20)
    at TestHook (http://localhost:9009/main.06d0c0c7c39d71512ee0.hot-update.js:28:73)
    at ProxyFacade (http://localhost:9009/vendors~main.fefa65282a203b643a70.bundle.js:369810:20)
    at mountIndeterminateComponent (http://localhost:9009/vendors~main.fefa65282a203b643a70.bundle.js:281585:13)
    at beginWork (http://localhost:9009/vendors~main.fefa65282a203b643a70.bundle.js:282085:16)
    at performUnitOfWork (http://localhost:9009/vendors~main.fefa65282a203b643a70.bundle.js:284842:12)
    at workLoop (http://localhost:9009/vendors~main.fefa65282a203b643a70.bundle.js:284882:24)
    at renderRoot (http://localhost:9009/vendors~main.fefa65282a203b643a70.bundle.js:284968:7)

I use the webpack plugin :

// .storybook/webpack.config.js
module.exports = (baseConfig, env, defaultConfig) => {
  defaultConfig.module.rules.push({
    test: /\.jsx?$/,
    include: /node_modules/,
    use: ['react-hot-loader/webpack']
  })

  return defaultConfig
}

I set the config to pureSFC :

// .storybook/config.js
import { setConfig } from 'react-hot-loader'
import { configure } from '@storybook/react'

setConfig({ pureSFC: true })

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

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

configure(loadStories, module)

And here is a minimal story using hooks which triggers the error :

import { useState } from 'react'
import { storiesOf } from '@storybook/react'

function TestHook() {
  let [state, setState] = useState(0)
  return (
    <button onClick={() => setState(state + 1)}>clicked {state} times</button>
  )
}

storiesOf('Test', module)
  .add('test', () => <TestHook />)

As long as you are using webpack loader - add โ€˜ignoreSFC:trueโ€™ to configuration.
But actually it should work even without it. Probably the setting is not actually set.

@theKashey I tried with a more manual way by aliasing react-dom and installing @hot-loader/react-dom and it works!

I'd prefer using the webpack's plugin but doing :

// .storybook/webpack.config.js
module.exports = (baseConfig, env, defaultConfig) => {
  defaultConfig.module.rules.push({
    test: /\.jsx?$/,
    include: /node_modules/,
    use: ['react-hot-loader/webpack']
  })
  return defaultConfig
}

doesn't seem to work.

I have no other issue with manipulating storybook's webpack config with other plugins.

React-Hot-Loader 4.6.0 should work with hooks out of the box.
https://medium.com/@antonkorzunov/react-hot-loader-4-6-41f3ce76fb08

The fix I've finally found is to force a specific version of @hot-loader/react-dom during the installation of the patch.

My current version of react-dom is 16.7.0-alpha.2, so I just did:

yarn add @hot-loader/[email protected]

and my .storybook/webpack.config.js is:

module.exports = (baseConfig, env, defaultConfig) => {

  // you can add more configs here...

  defaultConfig.resolve.alias['react-dom'] = '@hot-loader/react-dom';
  return defaultConfig;
};

And now hooks are working fine inside storybook!

React-Hot-Loader 4.6.0 should work with hooks out of the box.
https://medium.com/@antonkorzunov/react-hot-loader-4-6-41f3ce76fb08

I'm using 4.6.1 and if I don't alias react-dom, it doesn't work out of the box.

Using the webpack's plugin is not working, I get the Hooks can only be called inside the body of a function component error.

I'm using 4.6.1 and if I don't alias react-dom, it doesn't work out of the box.

@jgoux - just give me an example, and I will fix it. pureSFC is working well in my examples, but probably I miss your case.

PS: webpack loader should work, that's another strange thing.

@theKashey My example is just above โ˜๏ธ : https://github.com/storybooks/storybook/issues/4691#issuecomment-445244958

The 4.6.1 is working by only aliasing react-dom and doing nothing else, which is great! But I wasn't able to make it works using the webpack plugin with storybook.

@theKashey My example is just above โ˜๏ธ : #4691 (comment)

The 4.6.1 is working by only aliasing react-dom and doing nothing else, which is great! But I wasn't able to make it works using the webpack plugin with storybook.

try to add the alias inside the webpack.config like here https://github.com/storybooks/storybook/issues/4691#issuecomment-447570189

defaultConfig.resolve.alias['react-dom'] = '@hot-loader/react-dom';

I am not sure why are you complicating things like this. I've forced react packages to version 16.7.0-alpha.2 and it works just fine. If you are a lucky user of Yarn, just put this into your package.json. No need to tweak any configuration or mess with hot loader.

  "resolutions": {
    "react": "16.7.0-alpha.2",
    "react-dom": "16.7.0-alpha.2"
  }

You can see obvious culprits here. Since React is backward compatible, there is no harm in forcing the alpha version for everything.

[1/4] Why do we have the module "react"...?
=> Found "[email protected]"
info Has been hoisted to "react"
info This module exists because it's specified in "dependencies".
=> Found "@storybook/react#[email protected]"
info This module exists because "@storybook#react" depends on it.
=> Found "react-split-pane#[email protected]"
info This module exists because "@storybook#addon-info#@storybook#components#react-split-pane" depends on it.

@FredyC - the problem was about - you __can__ use hook, but they will not work due to _this issue_.

@theKashey I had the exact same problem as in OP and by forcing react versions it was gone. I assume it's because Storybook uses a hook-less version of React to drive the rendering. However, when you import eg. useState, it's a React 16.7 and it looks for the dispatcher which is most likely in some different space. I don't know internals that much.

From all these comments this workaround makes the most sense to me. I don't get why would hot loader be involved at all. I am on Storybook 4.1.2 and react-hot-loader is not even installed as a dependency. If some people have it in node_modules it's most likely for different reasons so any kind of hacking webpack config can doubtfully work.

You are lucky one, then :)

@FredyC it worked for me alhamdulillah! thanks

@domenico-ruggiano Yes I know that aliasing is working, that's what I use too.
I just wanted to point out that the webpack plugin approach isn't working with storybook. ๐Ÿ‘

@jgoux - sorry, still didn't get enough time to reproduce your example, but could you try to use unshift instead of push?

@theKashey Not sure what's going on but it's not working anymore, even with the alias solution.
Environment :

  • storybook v4.2.0-alpha.5 (also tried with v4.1.3, same error)
  • react-hot-loader v4.6.3
  • @hot-loader/react-dom 16.7.0
  • react-dom 16.7.0
  • react 16.7.0
Object(...) is not a function
    TypeError: Object(...) is not a function
    at TestHook (http://localhost:9009/main.f5798b722fe2ceddc6c1.bundle.js:88:73)
    at mountIndeterminateComponent (http://localhost:9009/vendors~main.f5798b722fe2ceddc6c1.bundle.js:16891:13)
    at beginWork (http://localhost:9009/vendors~main.f5798b722fe2ceddc6c1.bundle.js:17395:16)
    at performUnitOfWork (http://localhost:9009/vendors~main.f5798b722fe2ceddc6c1.bundle.js:20152:12)
    at workLoop (http://localhost:9009/vendors~main.f5798b722fe2ceddc6c1.bundle.js:20192:24)
    at renderRoot (http://localhost:9009/vendors~main.f5798b722fe2ceddc6c1.bundle.js:20278:7)
    at performWorkOnRoot (http://localhost:9009/vendors~main.f5798b722fe2ceddc6c1.bundle.js:21169:7)
    at performWork (http://localhost:9009/vendors~main.f5798b722fe2ceddc6c1.bundle.js:21081:7)
    at performSyncWork (http://localhost:9009/vendors~main.f5798b722fe2ceddc6c1.bundle.js:21055:3)
    at requestWork (http://localhost:9009/vendors~main.f5798b722fe2ceddc6c1.bundle.js:20924:5)

I used this minimal example :

import { useState } from 'react'
import { storiesOf } from '@storybook/react'

function TestHook() {
  let [state, setState] = useState(0)
  return (
    <button onClick={() => setState(state + 1)}>clicked {state} times</button>
  )
}

storiesOf('Test', module)
  .add('test', () => <TestHook />)

@jgoux Yea, this is latest gotcha :D React 16.7.0 was released without hooks and if you are not careful, it can break apps. See more info here: https://reactjs.org/blog/2018/12/19/react-v-16-7.html#why-is-this-bugfix-a-minor-instead-of-a-patch

@FredyC Wow I totally missed that! So it means that we need to stick to 16.7.0-alpha to keep hooks?

Yes alpha.2 version or __16.7.0-alpha.2.4__ in terms of hot-loader/react-dom

@theKashey Sorry for the noise, just tested with react 16.7.0-alpha.2 and @hot-loader/react-dom v16.7.0-alpha.2.4 and storybook v4.2.0-alpha.5 and it works in both cases (alias + webpack)! ๐Ÿ‘

EDIT : In fact, it's even working without aliasing or using the webpack plugin. ๐Ÿ˜ฎ

React-Hot-Loader 4.6.0 should work with hooks out of the box.

@theKashey Indeed! I was confused by this : https://github.com/gaearon/react-hot-loader#-hot-labs- :

(required) use webpack plugin to let RHL patch React-DOM for you.

You will have problems with memo and forwardRef - they may update wrapped component only on the second render (~ double buffering).

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!

I don't have a custom webpack config for storybook.
I don't have hot-loader installed.

i was able to make it work by forcing:
"@storybook/react": "4.2.0-alpha.5", in devDependencies
and
"react": "16.7.0-alpha.2",
"react-dom": "16.7.0-alpha.2" in dependencies

@gaurav5430, are you using create-react-app?

FYI: There's a new alpha of React with hooks: https://www.npmjs.com/package/react/v/16.8.0-alpha.1

No, i am not using create-react-app.

Regarding the new alpha with hooks, great, I would try that out.

This should work with the 4.1 release. According to the release notes, Storybook now uses the React versions from the project for the actual preview (and whatever it uses internally for the manager).

That means, if you update Storybook to 4.1 or later, and React + React-DOM to 16.8.0-alpha.1, you should have Hooks working in Storybook.

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!

For my, it worked:

const MyComponent = () => {
    const [number, setNumber] = useState(0);
    return (
        <div>
            <button onClick={() => setNumber(number + 1)}>Inc</button>
            <div>{number}</div>
        </div>
    )
};

storiesOf('MyStoreOf', module)
    .add('myComponent', () => (<MyComponent /> ));

react e react-dom: 16.8.1.

I guess this should be closed then @ilias-t

I was able to get hooks working, but unfortunately the knobs addon stopped working and the jsx addon just showed the story container component. To bad, it would be really nice to be able to manage the state for the sake of example.

@JasonTheAdams pretty sure @hipstersmoothie has something working with the jsx addon -- maybe he can chime in here?

@JasonTheAdams Here I describe a simple way to use hooks and still retain the jsx. It only works for child components though

https://mobile.twitter.com/HipsterSmoothie/status/1107182717467156484

My co worker built a tiny library with a hooks like API that he just released that is also jsx friendly. This is probably your best bet to get jsx and interactivity

https://github.com/adierkens/storybook-addon-state

Thanks, @hipstersmoothie! In your example, it doesn't just show <InputState /> in your Story Source? Does it show the <Input /> component inside or only that?

I really like your co-workers library, but I'm reluctant to use a complete recreation of useState, as he's not using the React useState at any point, which means he'd have to update this function to reflect any future changes React makes.

I really wish Storybook treated the function passed to the story as a functional component, allowing it to take full advantage of React.

Edit: To clarify, I switched from the JSX addon to the info addon, which does the same thing.

In your example, it doesn't just show in your Story Source? Does it show the component inside or only that?

the jsx addons should show Input and InputState. you can use the skip param to get skip state though.

as he's not using the React useState at any point, which means he'd have to update this function to reflect any future changes React makes.

I really wish Storybook treated the function passed to the story as a functional component, allowing it to take full advantage of React.

You are right on the money here. IDK if it would work but there might be some changes to @storybook/react that could support this. This is the reason when my coworkers library doesn't use react.useState

I hit the same problem using a hooks based component, and after some experimentation found the cause of it...

My story markup looked like this

<div style={{maxWidth: '280px'}}>
  <MyComponent/>
</div>

It appears that a normal <div> tag is what confuses the matter, and the solution is to make sure that the wrapping component is a React component, and not just a regular html tag. So I defined a Wrapper component like this:

const Wrapper = props => <div style={{maxWidth: '280px'}}>{props.children}</div>

and then changed my story markup to be like this

<Wrapper>
  <MyComponent>
</Wrapper>

Problem solved, and no need to import any special libraries to make it work.

I also had this problem, and tracked it down (as someone else said before) to the fact that Storybook uses a different instance of React (one in my workspace node_modules, and one in the storybook node_modules, I'm using Yarn Workspaces, monorepo).

I tried the package.json resolution trick, to no avail.

I got it working though by tweaking the webpack.config by using aliases and forcing both react and react-dom to be used from the root node_modules:

const aliases = {
react: path.resolve(__dirname, '../../../node_modules', 'react'),
 'react-dom': path.resolve(__dirname, '../../../node_modules', 'react-dom'),
};

and then in your config :

config.resolve.alias = aliases;

Hope that helps.

Hey, so this is a known issue with hooks, I believe they have it documented on the React side - you can also symlink react (if you're symlinking a package into your project - you symlink React from the project back to your package).

Wrapping the story in a div worked for me

storiesOf("Hooks", module)
  .add(
    "Hook",
    () => (
      <div>
        <Hook/>
      </div>
    ),
  );

I'm still facing this issue in storybook. Is this output from npm ls correct or not?
imageimage

In my case, the problem was caused by why-did-you-update module.

Hooks fully supported in 5.2. Upgrade to try it out. Here's the PR: https://github.com/storybookjs/storybook/pull/7571

@mikkelking's suggestion did the trick for me. Just having intermediary component solves the hooks issue.

@helsont you shouldn't need to do that anymore if you use a recent 5.2 beta thanks to #7571

Trying to use a hook in a custom addon is not working for me on 5.2.8
It does work in stories though.

Was this page helpful?
0 / 5 - 0 ratings