react_on_rails icon indicating copy to clipboard operation
react_on_rails copied to clipboard

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

Open lukaskamp opened this issue 5 years ago • 53 comments

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 ?

lukaskamp avatar Mar 05 '19 09:03 lukaskamp

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 : https://github.com/rails/webpacker/issues/1840

amauryfischer avatar Mar 08 '19 20:03 amauryfischer

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

justin808 avatar Mar 14 '19 00:03 justin808

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

wuz avatar Mar 22 '19 16:03 wuz

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.

amauryfischer avatar Mar 24 '19 17:03 amauryfischer

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

wuz avatar Mar 25 '19 19:03 wuz

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

amauryfischer avatar Mar 25 '19 20:03 amauryfischer

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.

juliusdelta avatar Apr 09 '19 19:04 juliusdelta

@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

amauryfischer avatar Apr 09 '19 21:04 amauryfischer

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.

janklimo avatar Apr 16 '19 18:04 janklimo

@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 avatar Apr 22 '19 10:04 rubiety

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.

rubiety avatar Apr 22 '19 11:04 rubiety

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.

janklimo avatar Apr 22 '19 12:04 janklimo

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.

justin808 avatar Apr 24 '19 18:04 justin808

@justin808 I have further checked the problem. several others also faced this issue with react hooks. ref. https://github.com/facebook/react/issues/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 ???

tahsin352 avatar May 11 '19 15:05 tahsin352

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.

filipkis avatar Jun 12 '19 23:06 filipkis

@filipkis nice finding, thanks

tahsin352 avatar Jun 13 '19 03:06 tahsin352

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.

batamire avatar Jun 24 '19 17:06 batamire

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

theocerutti avatar Oct 13 '19 15:10 theocerutti

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 avatar Oct 23 '19 18:10 aflansburg

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.

aflansburg avatar Mar 02 '20 16:03 aflansburg

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

jacksonrayhamilton avatar Mar 26 '20 02:03 jacksonrayhamilton

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

justin808 avatar Mar 26 '20 03:03 justin808

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.

jacksonrayhamilton avatar Mar 26 '20 16:03 jacksonrayhamilton

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 .

dchersey avatar Apr 15 '20 02:04 dchersey

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.

jacksonrayhamilton avatar Apr 15 '20 16:04 jacksonrayhamilton

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:

  • https://github.com/shakacode/react_on_rails/blob/master/node_package/src/ReactOnRails.ts#L40
  • https://github.com/shakacode/react_on_rails/blob/master/node_package/src/ComponentRegistry.ts#L21
  • https://github.com/shakacode/react_on_rails/blob/master/node_package/src/generatorFunction.ts#L11

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:

https://github.com/shakacode/react_on_rails/pull/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 avatar Apr 16 '20 02:04 justin808

Thanks for the offer Justin. We'll take you up on that if we need further help.

How about if we use Function.length?

I think that could work.

jacksonrayhamilton avatar Apr 17 '20 18:04 jacksonrayhamilton

if you want to fix this problem just use react:"^16.8.0" react-dom: "^16.8.0", react-scripts: "2.1.1", in package.json

sharjeel288 avatar Apr 23 '20 12:04 sharjeel288

@justin808 #1268 closes this issue, correct?

Judahmeek avatar Apr 28 '20 15:04 Judahmeek

Yes it does!

justin808 avatar Apr 29 '20 08:04 justin808