React: Bug: [17.0.1] Incorrectly throws Invalid hook call

Created on 23 Oct 2020  Â·  8Comments  Â·  Source: facebook/react

React version: 17.0.1
React Reconciler version: 0.26.1

Steps To Reproduce

  1. I upgraded from [email protected] to [email protected] and from [email protected] to [email protected]
  2. I am using custom made renderer with my hostconfig (see code example)

Link to code example: Unfortunately project is private, but I am including all related files below (click sections to expand)


Custom reconciler hostconfig (click to expand)

```ts
import danteApp from '$dante/util/danteApp';
import { diffProps, instances } from '$dante/util/helpers';
import { DanteInstance, InstanceType, PixiStage, UnknownProps } from '$dante/util/types';
import { isFunction } from 'lodash';
import { ReactNode } from 'react';
import ReactReconciler from 'react-reconciler';

const reconciler = ReactReconciler({
createInstance(type: T, props: UnknownProps) {
// @ts-expect-error
return new instancestype;
},

createTextInstance() {
throw new Error('dante does not support text instances. Use Text component instead.');
},

shouldSetTextContent() {
return false;
},

appendChild(parentInstance: DanteInstance, child: DanteInstance) {
parentInstance.addDanteChild(child);
},

appendChildToContainer(container: PixiStage, child: DanteInstance) {
container.addChild(child.instance);
},

appendInitialChild(parentInstance: DanteInstance, child: DanteInstance) {
parentInstance.addDanteChild(child);
},

insertBefore(parentInstance: DanteInstance, child: DanteInstance, beforeChild: DanteInstance) {
if (child === beforeChild) {
throw new Error('dante cannot insert node before itself');
}
const index = parentInstance.getDanteChildIndex(beforeChild);
const childExists = parentInstance.getDanteChildIndex(child) !== -1;
if (childExists) {
parentInstance.setDanteChildIndex(child, index);
} else {
parentInstance.addDanteChildAt(child, index);
}
},

insertInContainerBefore(container: PixiStage, child: DanteInstance, beforeChild: DanteInstance) {
if (child === beforeChild) {
throw new Error('dante cannot insert node before itself');
}
const index = container.getChildIndex(beforeChild.instance);
const childExists = container.getChildIndex(child.instance) !== -1;
if (childExists) {
container.setChildIndex(child.instance, index);
} else {
container.addChildAt(child.instance, index);
}
},

removeChild(parentInstance: DanteInstance, child: DanteInstance) {
parentInstance.removeDanteChild(child);
},

removeChildFromContainer(container: PixiStage, child: DanteInstance) {
container.removeChild(child.instance);
child.instance.destroy();
},

getPublicInstance(instance: DanteInstance) {
return instance;
},

getRootHostContext(rootContainerInstance) {
return rootContainerInstance;
},

getChildHostContext() {
return {};
},

prepareForCommit() {
return null;
},

prepareUpdate(_instance, _type, oldProps: UnknownProps, newProps: UnknownProps) {
return diffProps(oldProps, newProps);
},

commitUpdate(instance: DanteInstance, updatePayload: UnknownProps) {
if (isFunction(instance.applyProps)) {
instance.applyProps(updatePayload);
}
},

finalizeInitialChildren(parentInstance: DanteInstance, _type, props: UnknownProps) {
if (isFunction(parentInstance.finalizeChildren)) {
parentInstance.finalizeChildren(props);
}

return false;

},

now: window.performance.now,

setTimeout: window.setTimeout,

clearTimeout: window.clearTimeout,

resetAfterCommit() {
// Noop
},

// @ts-expect-error
clearContainer() {
return false;
},

hideInstance(instance: DanteInstance) {
instance.instance.renderable = false;
instance.instance.visible = false;
},

unhideInstance(instance: DanteInstance) {
instance.instance.renderable = true;
instance.instance.visible = true;
},

noTimeout: -1,
isPrimaryRenderer: true,
supportsMutation: true,
supportsPersistence: false,
supportsHydration: false
});

export function render(root: ReactNode) {
const container = reconciler.createContainer(danteApp.stage, false, false);
reconciler.updateContainer(root, container, null, () => null);
}

export const danteRenderer = danteApp.renderer;

</details>

<details>
  <summary>Hook related to the error (click to expand)</summary>

```ts
import { FONTS } from '$app/util/assets';
import theme from '$app/util/theme';
import { widthPercentage } from '$core/helpers';
import { BitmapFont, TextStyle } from 'pixi.js';
import { useCallback, useEffect, useState } from 'react';

/**
 * Utilities
 */
const { fonts } = document;
const fontSize = widthPercentage(theme.fontSize.large);
const fontOptions = {
  chars: BitmapFont.ALPHANUMERIC,
  resolution: devicePixelRatio,
  padding: widthPercentage(2)
};
const gold = new TextStyle({
  fill: theme.gradient.gold,
  fillGradientStops: [0.1, 0.5, 1],
  fillGradientType: 0,
  fontSize,
  ...theme.shadow.textShadowDark
});

/**
 * Hook
 */
function useFonts() {
  const [ready, setReady] = useState(false);

  const loadFonts = useCallback(async () => {
    // Load custom fonts
    const openSans = new FontFace('OpenSans', `url(${FONTS.OpenSans})`, {
      weight: 'normal',
      style: 'normal'
    });

    fonts.add(openSans);

    await openSans.load();

    // Create optimised bitmap fonts
    BitmapFont.from('Script-gold', { ...gold, fontFamily: 'OpenSans' }, fontOptions);

    setReady(true);
  }, []);

  useEffect(() => {
    loadFonts();
  }, [loadFonts]);

  return ready;
}

export default useFonts;


Hook usage in the application (click to expand)

import LayoutRouteTransition from '$app/components/LayoutRouteTransition';
import Route from '$app/components/Route';
import useFonts from '$app/hooks/useFonts';
import Game from '$app/layouts/Game';
import Registration from '$app/layouts/Registration';
import React, { Fragment } from 'react';

function App() {
  const fonts = useFonts();

  if (!fonts) {
    return null;
  }

  return (
    <LayoutRouteTransition>
      {layout => (
        <Fragment>
          <Route route={layout} match="Registration" Component={Registration} />
          <Route route={layout} match="Game" Component={Game} />
        </Fragment>
      )}
    </LayoutRouteTransition>
  );
}

export default App;

The current behavior

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
    See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
    at resolveDispatcher (react.development.js:1476)
    at useState (react.development.js:1507)
    at useFonts (useFonts.ts:39)
    at App (App.tsx:10)
    at renderWithHooks (react-reconciler.development.js:6412)
    at mountIndeterminateComponent (react-reconciler.development.js:9238)
    at beginWork (react-reconciler.development.js:10476)
    at HTMLUnknownElement.callCallback (react-reconciler.development.js:12184)
    at Object.invokeGuardedCallbackDev (react-reconciler.development.js:12233)
    at invokeGuardedCallback (react-reconciler.development.js:12292)

The expected behavior

As far as I can tell this should be valid?

Unconfirmed

Most helpful comment

More concretely, it usually means that react resolved from your component code is a different version/copy than react resolved from the reconciler.

All 8 comments

Sorry, we can't really guess without a reproducing example, but this error usually means one thing — two copies of React.

More concretely, it usually means that react resolved from your component code is a different version/copy than react resolved from the reconciler.

Hi, I hope you are well. I am not sure how I ended up on this subscribe
list but can you please remove me? Thank you!

On Fri, Oct 23, 2020 at 7:56 AM Dan Abramov notifications@github.com
wrote:

More concretely, it usually means that react resolved from your component
code is a different version/copy than react resolved from the reconciler.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/facebook/react/issues/20082#issuecomment-715296698,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAWUWA2JRUSU34FZMK63H2TSMFVQBANCNFSM4S4KBTNQ
.

>

David Webb
Cell: 860.248.5632

Sent from iPhone

@nlaffey We can't subscribe or unsubscribe you, but the part you quoted contains an unsunscribe link so you can press it?

There's a good note in the React docs about a workaround with Yarn resolutions. That fixed this for me!

E.g. adding this to package.json, forcing packages to resolve to newer versions of React:

{
    "resolutions": {
        "react": "^17.0.1",
        "react-dom": "^17.0.1"
    }
}

Seems like there's no issue here on our side.

I wonder if now that we made peerDependencies stricter (exact version), npm started to fail deduping during upgrades, resulting in duplicates.

@gaearon I was using react-reconciler in yarn workspace package separate from my app project. I had react: "*" in reconcilers package as a peerDep, setting that to exact version of react (17.0.1) resolved this.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

brunolemos picture brunolemos  Â·  285Comments

fdecampredon picture fdecampredon  Â·  139Comments

gabegreenberg picture gabegreenberg  Â·  264Comments

gaearon picture gaearon  Â·  126Comments

wohali picture wohali  Â·  128Comments