diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index feaf672b9bdb3..9d20d062461fe 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -360,32 +360,50 @@ const tests = { { code: normalizeIndent` function MyComponent(props) { - useCustomEffect(() => { + useCustomHook(() => { console.log(props.foo); }); } `, - options: [{additionalHooks: 'useCustomEffect'}], + options: [{additionalHooks: 'useCustomHook'}], }, { code: normalizeIndent` function MyComponent(props) { - useCustomEffect(() => { + useCustomHook(() => { console.log(props.foo); }, [props.foo]); } `, - options: [{additionalHooks: 'useCustomEffect'}], + options: [{additionalHooks: 'useCustomHook'}], }, { code: normalizeIndent` function MyComponent(props) { - useCustomEffect(() => { + useCustomHook(() => { console.log(props.foo); }, []); } `, - options: [{additionalHooks: 'useAnotherEffect'}], + options: [{additionalHooks: 'useAnotherHook'}], + }, + { + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }); + } + `, + }, + { + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, [props.foo]); + } + `, }, { // Valid because we don't care about hooks outside of components. @@ -3002,6 +3020,105 @@ const tests = { }, ], }, + { + code: normalizeIndent` + function MyComponent(props) { + useCustomHook(() => { + console.log(props.foo); + }, []); + useEffect(() => { + console.log(props.foo); + }, []); + React.useEffect(() => { + console.log(props.foo); + }, []); + React.useCustomHook(() => { + console.log(props.foo); + }, []); + } + `, + options: [{additionalHooks: 'useCustomHook'}], + errors: [ + { + message: + "React Hook useCustomHook has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useCustomHook(() => { + console.log(props.foo); + }, [props.foo]); + useEffect(() => { + console.log(props.foo); + }, []); + React.useEffect(() => { + console.log(props.foo); + }, []); + React.useCustomHook(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook useEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useCustomHook(() => { + console.log(props.foo); + }, []); + useEffect(() => { + console.log(props.foo); + }, [props.foo]); + React.useEffect(() => { + console.log(props.foo); + }, []); + React.useCustomHook(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook React.useEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useCustomHook(() => { + console.log(props.foo); + }, []); + useEffect(() => { + console.log(props.foo); + }, []); + React.useEffect(() => { + console.log(props.foo); + }, [props.foo]); + React.useCustomHook(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, + ], + }, { code: normalizeIndent` function MyComponent(props) { @@ -3019,7 +3136,6 @@ const tests = { }, []); } `, - options: [{additionalHooks: 'useCustomEffect'}], errors: [ { message: @@ -4054,6 +4170,36 @@ const tests = { ], options: [{additionalHooks: 'useLayoutEffect_SAFE_FOR_SSR'}], }, + { + code: ` + function MyComponent() { + const myRef = useRef(); + useIsomorphicLayoutEffect(() => { + const handleMove = () => {}; + myRef.current.addEventListener('mousemove', handleMove); + return () => myRef.current.removeEventListener('mousemove', handleMove); + }); + return
; + } + `, + output: ` + function MyComponent() { + const myRef = useRef(); + useIsomorphicLayoutEffect(() => { + const handleMove = () => {}; + myRef.current.addEventListener('mousemove', handleMove); + return () => myRef.current.removeEventListener('mousemove', handleMove); + }); + return ; + } + `, + errors: [ + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + ], + }, { // Autofix ignores constant primitives (leaving the ones that are there). code: normalizeIndent` diff --git a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js index d2cb068562046..c96efa08002cc 100644 --- a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js +++ b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js @@ -1504,7 +1504,9 @@ function getReactiveHookCallbackIndex(calleeNode, options) { // useImperativeHandle(ref, fn) return 1; default: - if (node === calleeNode && options && options.additionalHooks) { + if (node === calleeNode && node.name.match(/use.+Effect/)) { + return 0; + } else if (node === calleeNode && options && options.additionalHooks) { // Allow the user to provide a regular expression which enables the lint to // target custom reactive hooks. let name;