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

React Hooks (Hooks can only be called inside the body of a function component) #1198

Closed
lukaskamp opened this issue Mar 5, 2019 · 54 comments

Comments

@lukaskamp
Copy link

I'm trying Testing React Hooks with react_on_rails gem and noticed there is a Error:

"Hooks can only be called inside the body of a function component error."

Without using ReactOnRails config (e.g in packs/applications.js) everything works correctly. Also when I used react-rails gem everything works.

Where could be problem ?

@amauryfischer
Copy link

Got same problem there, i tried a couple of react suggestion(veryfing i'm using hooks correctly, and veryfing that i got a single version of React) but it's not one of them...
Seems we are not alone, don't know if it comes from rails webpacker, React on rails, or anything else.
Related issue on rails webpacker : rails/webpacker#1840

@justin808
Copy link
Member

@lukaskamp @amauryfischer can one of you give me a simple reproduction case? a github repo?

@wuz
Copy link

wuz commented Mar 22, 2019

As a note, a quick fix for this is to just wrap your React On Rails rendered component in a function, like this:

export default props => <ComponentHere {...props} />;

@amauryfischer
Copy link

import React,{useState} from 'react'
const Test = function() {
    const [state, setstate] = useState(0);
    return (
        <div onClick={setstate(state + 1,)}>{state}</div>
    )
}

export default props => <Test {...props} />;

@wuz didn't work, same error :

Invariant Violation: Hooks can only be called inside the body of a function component.

@wuz
Copy link

wuz commented Mar 25, 2019

@amauryfischer Is that the exact code you are using? That setstate call in the button won't work, since that is just getting called on render, causing an infinite loop.

Using export default props => <Test {...props} />; worked for me.

@amauryfischer
Copy link

amauryfischer commented Mar 25, 2019

Yes my bad @wuz , but even corrected

import React,{useState} from 'react'
const Test = function() {
    const [state, setstate] = useState(0);
    return (
        <div onClick={() => setCount(count + 1)}>{state}</div>
    )
}

export default props => <Test {...props} />;

got the same error.
Even with only

<div>try</div> 

in the return.
This is so strange ><

@juliusdelta
Copy link

We're having the same issue.

We have multiple components rendering on the same page. We believe this is related to the issue described in the react docs about loading 2 instances of React, possibly.

The easiest way to reproduce is to make 2 components with hooks and register them then pass those two into a Rails view. If I have enough time later today I'll make a reproduction repo and update this comment.

@amauryfischer
Copy link

@juliusdelta Exactly ! I love you so much. Finally find out why my hooks wasn't working thanks to you.
i had a javascript_pack_tag with a component registered in application.html.erb and it brokes my whole components for each view due to multiple React for each view.

I'm happy, was searching for this since a whole month

@janklimo
Copy link

janklimo commented Apr 16, 2019

I've put together a minimal reproduction repo that can be found here.

As suggested by @wuz above, the error is thrown based on the way we export the component:

// Works ✅
export default props => <HelloWorld {...props} />;

// Fails 🔴
export default HelloWorld;

Here's the error message:

Screen Shot 2019-04-17 at 1 01 42 AM

I've tried using the latest react and react-dom versions but it didn't help. There is only one react component embedded on the hello_world page. Turbolinks are not included.

I've been recently dealing with this problem in our app (not usingreact_on_rails) using Turbolinks and it appears that rendering the component in both preview and final render causes some sort of race condition. Not rendering react components in preview seems to do the trick for us but it's interesting to see it's not a Turbolinks issue, so happy to help work on this to resolve the root cause.

@rubiety
Copy link

rubiety commented Apr 22, 2019

@janklimo's workaround above using export default props => <HelloWorld {...props} />; does indeed seem to work for the case where there is only one react_component on the page, and I am using it successfully in that case.

Unfortunately, when the page contains multiple react_component invocations (for a component using hooks), this appears to not work, as is pointed out by @juliusdelta and @amauryfischer. Even creating an explicit wrapper class-based component, that just delegate props the functional component using hooks, doesn't appear to work either.

So as it stands right now, it appears to just not be possible to use react hooks while rendering multiple components on the same page. This is a pretty serious problem, and seems to mean the only way to work around this is to rewrite components without using hooks at all (at any level of the component tree, when using multiple components o the same page).

@rubiety
Copy link

rubiety commented Apr 22, 2019

After some further investigation on this, and reading this issue carefully, I realized my particular issue was the inclusion of multiple bundles on the same page. The problem with two or more javascript_pack_tag's on the same page is that if more than one bundle includes a copy of react, then this will cause problems with React hooks.

I was able to work around this problem by consolidating multiple packs into one to ensure that only one React copy was loaded. After that, @janklimo's fix using export default props => <HelloWorld {...props} />; does indeed work around this problem.

@janklimo
Copy link

Thank you for the update @rubiety 👍 While evaluating our options I tested numerous gems for using React with Rails and react-rails seems to have hooks figured out (my demo and repo). Not meaning to promote alternative gems, just thought it could help identify the problem.

@justin808
Copy link
Member

justin808 commented Apr 24, 2019

I was able to work around this problem by consolidating multiple packs into one to ensure that only one React copy was loaded. After that, @janklimo's fix using export default props => <HelloWorld {...props} />; does indeed work around this problem.

@rubiety Having multiple copies of React sent to the browser is also bad for performance. So definitely having on copy of React is correct.

I'm puzzled by why react-rails would not have issues. I looked at the source code there and I could not find anything notable.

@janklimo I really appreciate your help on this issue. If anybody trying to debug this wants to pair with me, please schedule a time with my calendar link or email me for a Slack invite.

@tahsin352
Copy link
Contributor

@justin808 I have further checked the problem. several others also faced this issue with react hooks. ref. facebook/react#13991

To solve 'multiple instances of react' error, I have tried some other ways like below:

#/config/webpack/custom.js
const path = require('path')

module.exports = {
  resolve: {
    alias: {
      'react': path.resolve('./node_modules/react'),
      'react-dom': path.resolve('./node_modules/react-dom')
    }
  }
}

# config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const customConfig = require('./custom')

environment.config.merge(customConfig)

again, I have also tried with npm link package. but it also didn't solve the error.
another solution i tried, using 'resolved_path' of webpacker, no luck. another way is 'nohoist' or 'externals' property of webpak.

Can i symlinked dependencies use their own copy of react in a react_on_rails gem file ???

@filipkis
Copy link

I had the same problem which occurred when I tried to render a second component. Few hours of debugging later I realised that the problem was not related to having two components actually, but the fact that a root component (the one that I put in my view) was the one using useState hook.

As soon as I wrapped it in another component that didn't use the hook all was fine with the world again.

In other words, hooks for some reason can't be in root component that you are rendering with react_on_rails.

@tahsin352
Copy link
Contributor

@filipkis nice finding, thanks

@batamire
Copy link

batamire commented Jun 24, 2019

I migrated from webpacker 3.6.0 to 4.0.7 and started seeing this error. Maybe this can help track down the issue? I have only one component in my view, wrapping does work.

@theocerutti
Copy link

Any news about this ? Because I can't use hooks anywhere..

@aflansburg
Copy link

We have been noticing this issue as well, it seems to occur possibly when we use @material-ui/core and some of its helpers such as withStyles() or possibly makeStyles() - the fix we are using for now is to wrap our export in a HOC:

export default props => <FunctionComponent {...props} />;

@aflansburg
Copy link

Was going to add, if you come up on this problem just try this:

function YourComponent(props){ // implementation }

export default props => <YourComponent {...props} />

As of yet I have no idea why that works. Not sure if this project is being actively maintained anymore.

@jacksonrayhamilton
Copy link

I think the issue is that React on Rails will call a function component like a normal function here, but the hooks code doesn’t expect for components to be used like that; I think it only expects function components to be passed to React.createElement.

In light of this @justin808, what do you think about removing this snippet?

  if (generatorFunction) {
    return component(props, railsContext);
  }

@justin808
Copy link
Member

@jacksonrayhamilton React on Rails allows you to create a function that takes 2 params, the props and the railsContext, and then your function should return a React component.

So I'm not totally clear on what you're suggesting.

Can you create a simple example to demonstrate this?

@jacksonrayhamilton
Copy link

Sure. In my pack file I have a function component that uses a hook, and I register this component:

import React, { useState } from 'react'

function HelloWorld() {
  const [whom] = useState('Everyone')
  return <div>Hello {whom}!</div>
}

ReactOnRails.register({ HelloWorld })

In my Rails template I render it:

<%= react_component('HelloWorld', prerender: false) %>

When I load up the page, I see that when my function component is being registered, ReactOnRails#register does not distinguish between a function component and a “generator function.”

Thus when I advance to the error, we can see that React is unhappy about useState being called…

Because a function component was called outside the React rendering process…

And thus useState was also called outside the React rendering process.

So I think the key is that “ReactOnRails#register does not distinguish between a function component and a ‘generator function.’” And perhaps that is not even possible to do reliably.

@dchersey
Copy link

This is nuts ... if anyone wants a working example of HMR with react-rails that supports hooks, look at this: https://github.com/Leap-Forward/react-rails-hmr .

@jacksonrayhamilton
Copy link

Well, I don’t think react_on_rails users need to resort to a whole new gem to fix their problem. I think in my last comment here I narrowed in on the problem and I expect it will be pretty easy to solve in the react_on_rails source. Maybe with a breaking API change at worst? I expect @justin808 has just been busy. Hooks are relatively new. I respect his choices in prioritizing his time.

@justin808
Copy link
Member

Awesome to hear from you all. We're working on some updates to the sample apps.

@jacksonrayhamilton I think you hit the nail on the head! The core issue is that we would like a way that we can infer if a function is for a React Component or just a function.

So the source links:

Either we have to fix the generatorFunction to be able to detect the difference, or we need to update the API so that we can specify generator function or a simple functional component.

How about if we use [Function.length](the https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length)?

If the length is 2, then we assume a generator function that will take two args:

  1. Props
  2. RailsContext

If the length is 3, we already assume that this is a "renderer" which is used for lazy loading.

If the length is zero or 1, then we can assume that this is simple React Component.

Thus, we can then call the React.createElement API

I took a try at this fix:

#1268

@behraaangm @Judahmeek @ashgaliyev can you guys please comment?

@jacksonrayhamilton thank you for your contribution!. Do you want a one-month free subscription to React on Rails Pro? I'm going to formalize this more soon on the main https://www.shakacode.com website.

@justin808
Copy link
Member

I merged #1280.

It console.errors with a detailed message. I'll just throw for the following major version.

I want to ship v12 soon!

@justin808
Copy link
Member

@jacksonrayhamilton @sharjeel288 @dchersey @filipkis @lukaskamp @amauryfischer @wuz @juliusdelta @janklimo @rubiety @tahsin352 @batamire @theocerutti @aflansburg Any of you want to help me test the master branch? I think we're close.

I could also create a beta version on rubygems and npm if one of you wanted that.

@justin808
Copy link
Member

@justin808
Copy link
Member

Fixed in v12!

@justin808
Copy link
Member

@jacksonrayhamilton @sharjeel288 @dchersey @filipkis @lukaskamp @amauryfischer @wuz @juliusdelta @janklimo @rubiety @tahsin352 @batamire @theocerutti @aflansburg

Hey, hey, React on Rails v12 has shipped!

@jacksonrayhamilton
Copy link

Awesome, thanks @justin808!

@imgarylai
Copy link
Contributor

I'm running a new Rails app with ruby 2.7.1 and Rails 6.0.3.4. I followed this tutorial and got the same error message as well.

Uncaught Invariant Violation: ReactOnRails encountered an error while rendering component: HelloWorld.
Original message: Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)
    at invariant (http://localhost:3000/packs/js/hello-world-bundle-842597fa5db1436cba9a.js:33684:19)
    at resolveDispatcher (http://localhost:3000/packs/js/hello-world-bundle-842597fa5db1436cba9a.js:35069:32)
    at useState (http://localhost:3000/packs/js/hello-world-bundle-842597fa5db1436cba9a.js:35093:24)
    at HelloWorld (http://localhost:3000/packs/js/hello-world-bundle-842597fa5db1436cba9a.js:121:73)
    at createReactElement (http://localhost:3000/packs/js/hello-world-bundle-842597fa5db1436cba9a.js:33323:12)
    at render (http://localhost:3000/packs/js/hello-world-bundle-842597fa5db1436cba9a.js:33132:75)
    at forEach (http://localhost:3000/packs/js/hello-world-bundle-842597fa5db1436cba9a.js:33048:5)
    at forEachComponent (http://localhost:3000/packs/js/hello-world-bundle-842597fa5db1436cba9a.js:33061:3)
    at reactOnRailsPageLoaded (http://localhost:3000/packs/js/hello-world-bundle-842597fa5db1436cba9a.js:33169:3)
    at HTMLDocument.renderInit (http://localhost:3000/packs/js/hello-world-bundle-842597fa5db1436cba9a.js:33194:5)

Gemfile:

...
gem 'react_on_rails', '12.0.3'
gem 'webpacker', '~> 5.2'
...
➜  ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin19]
➜  rails -v
Rails 6.0.3.4

@justin808
Copy link
Member

@imgarylai Can you please share your repo on Github. If you're 100% sure this is an issue, please open a new issue.

@justin808 justin808 reopened this Nov 7, 2020
@justin808
Copy link
Member

Might be related to React v17. The tutorial steps will get the latest React.

https://github.com/facebook/react/blob/master/CHANGELOG.md#1700-october-20-2020

@justin808
Copy link
Member

@imgarylai can you try to adjust the steps so that you don't use React 17 to isolate if that is the cause?

@imgarylai
Copy link
Contributor

@justin808 Thank you! I will try it again and use React 16

@jwcatprint
Copy link

@justin808 I received the same error with the demo when installing on a new app

@justin808
Copy link
Member

@justin808 I received the same error with the demo when installing on a new app

@jwcatprint are you using React 17? I'll look into this no later than this coming weekend.

In the meantime can somebody confirm that

solves the issue.

@justin808
Copy link
Member

PRs welcome!

@justin808
Copy link
Member

Moving over to a new issue #1336.

@justin808
Copy link
Member

This should have been fixed in 12.0.4.

@samtgarson
Copy link

In case anyone else is stuck on the same issue I am (unrelated to other fixes so far):

I've been getting this on 12.2... turned out to be because of this issue which is caused by adding multiple calls to javascript_pack_tags on one page, leading to multiple instances of React.

For now, the fix is to only call javascript_pack_tag once per page.

@justin808 justin808 reopened this Jun 14, 2021
@justin808
Copy link
Member

I'm reopening this issue just to make it clear that javascript_pack_tag needs to be only once on the view, including the layout when using rails/webpacker v6.

@Judahmeek Judahmeek changed the title React Hooks React Hooks (Hooks can only be called inside the body of a function component) Jul 9, 2021
@vtamara
Copy link

vtamara commented Mar 31, 2022

Referencing shakacode/shakapacker#39 because solving that would help here.

@justin808
Copy link
Member

Fixed a long time ago.

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