Issue 1:
React version: 17.0.1

React Reconciler version: 0.26.1

Steps To Reproduce

I upgraded from react@17.0.0 to react@17.0.1 and from react-reconciler@0.26.0 to react-reconciler@0.26.1

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)

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<T extends InstanceType>(type: T, props: UnknownProps) {
// @ts-expect-error
return new instances[type](props);
},

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;

Hook related to the error (click to expand)

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:

You might have mismatching versions of React and the renderer (such as React DOM)

You might be breaking the Rules of Hooks

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?

Issue 2:
When a function passed through props is used inside useCallback, the react-hooks/exhaustive-deps eslint rule give a false positive.

Using a value on the props object or reference the function doesn't generate a warning. Only when the function is called. See example below.

React version: 17.0.1

eslint-plugin-react-hooks: 4.6.0

Steps To Reproduce

The following code causes a linting error:

import React, { useCallback } from 'react';

interface TestComponent {
value: number;
testFunc: () => void;
}

export const TestComponent: React.FC<TestComponent> = (props) => {
const onClick1 = useCallback(() => {
props.testFunc();
}, [props.testFunc]); // Error here

const onClick2 = useCallback(() => {
console.log(props.value);
}, [props.value]); // No error here

const onClick3 = useCallback(() => {
console.log(props.testFunc);
}, [props.testFunc]); // No error here

return <div>
<div onClick={onClick1}></div>
<div onClick={onClick2}></div>
<div onClick={onClick3}></div>
</div>
};

Link to code example:

The current behavior

eslint produces an error

React Hook useCallback has a missing dependency: 'props'. Either include it or remove the dependency array. However, 'props' will change when *any* prop changes, so the preferred fix is to destructure the 'props' object outside of the useCallback call and refer to those specific props inside useCallback.

The expected behavior

No error on all 3 cases.