diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index fc343705a6922..2de2daa85d656 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -7635,15 +7635,53 @@ if (__EXPERIMENTAL__) { ...tests.valid, { code: normalizeIndent` - function MyComponent({ theme }) { - const onStuff = useEvent(() => { - showNotification(theme); - }); - useEffect(() => { - onStuff(); - }, []); - } - `, + function MyComponent({ theme }) { + const onStuff = useEvent(() => { + showNotification(theme); + }); + useEffect(() => { + onStuff(); + }, []); + } + `, + }, + ]; + + tests.invalid = [ + ...tests.invalid, + { + code: normalizeIndent` + function MyComponent({ theme }) { + const onStuff = useEvent(() => { + showNotification(theme); + }); + useEffect(() => { + onStuff(); + }, [onStuff]); + } + `, + errors: [ + { + message: + 'Functions returned from `useEvent` must not be included in the dependency array. ' + + 'Remove `onStuff` from the list.', + suggestions: [ + { + desc: 'Remove the dependency `onStuff`', + output: normalizeIndent` + function MyComponent({ theme }) { + const onStuff = useEvent(() => { + showNotification(theme); + }); + useEffect(() => { + onStuff(); + }, []); + } + `, + }, + ], + }, + ], }, ]; } diff --git a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js index 6f99e6ef4369e..0885d268367f8 100644 --- a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js +++ b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js @@ -74,6 +74,7 @@ export default { const stateVariables = new WeakSet(); const stableKnownValueCache = new WeakMap(); const functionWithoutCapturedValueCache = new WeakMap(); + const useEventVariables = new WeakSet(); function memoizeWithWeakMap(fn, map) { return function(arg) { if (map.has(arg)) { @@ -226,7 +227,12 @@ export default { // useRef() return value is stable. return true; } else if (isUseEventIdentifier(callee) && id.type === 'Identifier') { - // useEvent() return value is stable. + for (const ref of resolved.references) { + if (ref !== id) { + useEventVariables.add(ref.identifier); + } + } + // useEvent() return value is always unstable. return true; } else if (name === 'useState' || name === 'useReducer') { // Only consider second value in initializing tuple stable. @@ -639,6 +645,26 @@ export default { }); return; } + if (useEventVariables.has(declaredDependencyNode)) { + reportProblem({ + node: declaredDependencyNode, + message: + 'Functions returned from `useEvent` must not be included in the dependency array. ' + + `Remove \`${context.getSource( + declaredDependencyNode, + )}\` from the list.`, + suggest: [ + { + desc: `Remove the dependency \`${context.getSource( + declaredDependencyNode, + )}\``, + fix(fixer) { + return fixer.removeRange(declaredDependencyNode.range); + }, + }, + ], + }); + } // Try to normalize the declared dependency. If we can't then an error // will be thrown. We will catch that error and report an error. let declaredDependency;