pmndrs / react-three-fiber

🇨🇭 A React renderer for Three.js
https://docs.pmnd.rs/react-three-fiber
MIT License
27.23k stars 1.56k forks source link

Hooks error in a class component even without using hooks. #860

Closed trusktr closed 3 years ago

trusktr commented 3 years ago

I'm not using Hooks, but when I use an intrinsic element like <mesh> inside a React.Component, I get the familiar hooks error

Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app

It happens on this first line of the Canvas component:

  const canvasRef = useRef(); // useRef throws the error.

I checked, but npm ls shows a single "deduped" react in node_modules, so I don't have duplicates, I only have a single version of React.

If I place the r3f markup in a separate function component, then use the function component inside the class component, I get the same error.

trusktr commented 3 years ago

I get the error even if I write code like this:

class Foo extends React.Component() {
  render() {
    return <div>{
      this.timelineState.filmstripItems.map(_item => <FilmstripThumbnail></FilmstripThumbnail>)
    }</div>
  }
}

function FilmstripThumbnail() {
  return (
    <div className="filmstripThumb">
      <Canvas>
        <perspectiveCamera position={[0, 0, -5]}></perspectiveCamera>
        <mesh rotation={[30, 30, 30]}>
          <boxGeometry args={[1, 1, 2]}></boxGeometry>
          <meshBasicMaterial color={'deeppink'}></meshBasicMaterial>
        </mesh>
      </Canvas>
    </div>
  )
}

No hooks there.

trusktr commented 3 years ago

Ah, indeed, I had a duplicate react in an npm linked module. This is a downside of using npm link to develop dependency packages in an app with React hooks.

When I unlinked the package, and installed it from git instead, the problem went away.

Wish React handled that case gracefully at runtime.

drcmda commented 3 years ago

i run into this all the time with aliasing, super hard to figure out what actually happend. glad you solved it!

why class components, though?

function Foo() {
  return (
    <div>
      {this.timelineState.filmstripItems.map(_item => <FilmstripThumbnail />)}
    </div>
  )
}

?

trusktr commented 3 years ago

why class components, though?

Well, since you asked, I'd be happy to explain a little. :smiley:

For one, some apps I'm in pre-date Hooks, with many class components.

In these cases, classes like Foo may be classes with various instance properties and methods. Classes like Foo may even have properties and methods that non-React code accesses (f.e. instances get passed out of a root component so that any non-React app can call methods or do other non-reactish things; value-holding props may not feel nice as a replacement for method calls, etc).

Plus classes have features that I personally like better.

In many ways, Hooks are a re-invention of classes using functions, with some added benefits like composability in a functional way, but with downsides like not having references for times when those are useful. I prefer to live with a little extra verbosity and follow conventional object-based approaches (f.e. an instance-property-based approach like mobx, which helps to get rid of the verbosity of immutable state, yay!) and I simply avoid the pitfalls of classes like not using old React mixins (while Hooks have other pitfalls).

I find it easier to teach new programmers how classes work than to teach them how React-specific Hooks work; and then that class knowledge they gain flows into other places outside of React whereas Hooks does not. Hooks requires a more "just write it like this and assume magic" approach, especially for brand-new programmers or those people who just want to get work done even if they are experienced devs in general but have no React experience (f.e. maybe they came from the backend world and need to make some functional UI even if it looks ugly, leaving the rest up to the UI expert).

trusktr commented 3 years ago

Well, since we're on this related topic....

npm link is a core part of how some projects I'm working on operate. Unfortunately I may not be able to introduce react-three-fiber if any taem members will have issues with the existing npm link workflows. I might have to roll back to plain Three.js for those parts, for now.

It's not an issue in r3f, but in React.

It's easy to solve with global cache variables that reference React's internal APIs. Duplicate React instances can grab the internals off of the globals, and they can also check if non-semver-compatible React's are in the app, and if so, throw a useful warning or error that things may break if incompatible Reacts are mixed together. If the duplicates are detected to all be semver-compatible, everything will be fine and no warnings/errors need to be shown to the user, and all React instances will use the shared singletons.

I've used this technique with success in my packages so that consumer apps wouldn't have duplicate dependency issues.

Dupe modules have been more notorious (in my experience) when using Meteor's module loader (reify by @benjamn) for some reason, and the approach I described helped solve issues in Meteor apps using my dependencies, and but it also helps with npm linked projects in general.

drcmda commented 3 years ago

if you look into our examples folder here, that might also be a good alternative to npm link. its using customize-cra to alias out of the source dir for better local debugging - without ejecting. don't know why, i always seemed to have trouble with npm link.

trusktr commented 3 years ago

customize-cra to alias

Thanks! Indeed, a Webpack resolve.alias may help here for projects with Webpack.

i always seemed to have trouble with npm link.

Yeah, it works great in general until stuff like the following happen (not React-specific):

I don't remember the links off hand, but I opened some feature requests on npm and typescript to help out with these cases. But it hasn't been high priority for them.

They overhauled npm link in npm v7, but I haven't had a chance to use it yet due to other critical issues; I've needed to revert back to npm v6 the few times I've tried so far.