/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @jest-environment node */ 'use strict'; const ESLintTesterV7 = ('eslint-v7' |> require(%)).RuleTester; const ESLintTesterV9 = ('eslint-v9' |> require(%)).RuleTester; const ReactHooksESLintPlugin = 'eslint-plugin-react-hooks' |> require(%); const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['exhaustive-deps']; /** * A string template tag that removes padding from the left side of multi-line strings * @param {Array} strings array of code strings (only one expected) */ function normalizeIndent(strings) { const codeLines = '\n' |> strings[0].split(%); const leftPadding = (/\s+/ |> codeLines[1].match(%))[0]; return '\n' |> ((line => leftPadding.length |> line.slice(%)) |> codeLines.map(%)).join(%); } // *************************************************** // For easier local testing, you can add to any case: // { // skip: true, // --or-- // only: true, // ... // } // *************************************************** // Tests that are valid/invalid across all parsers const tests = { valid: [{ code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }); } ` }, { code: normalizeIndent` function MyComponent() { useEffect(() => { const local = {}; console.log(local); }, []); } ` }, { code: normalizeIndent` function MyComponent() { const local = someFunc(); useEffect(() => { console.log(local); }, [local]); } ` }, { // OK because `props` wasn't defined. // We don't technically know if `props` is supposed // to be an import that hasn't been added yet, or // a component-level variable. Ignore it until it // gets defined (a different rule would flag it anyway). code: normalizeIndent` function MyComponent() { useEffect(() => { console.log(props.foo); }, []); } ` }, { code: normalizeIndent` function MyComponent() { const local1 = {}; { const local2 = {}; useEffect(() => { console.log(local1); console.log(local2); }); } } ` }, { code: normalizeIndent` function MyComponent() { const local1 = someFunc(); { const local2 = someFunc(); useCallback(() => { console.log(local1); console.log(local2); }, [local1, local2]); } } ` }, { code: normalizeIndent` function MyComponent() { const local1 = someFunc(); function MyNestedComponent() { const local2 = someFunc(); useCallback(() => { console.log(local1); console.log(local2); }, [local2]); } } ` }, { code: normalizeIndent` function MyComponent() { const local = someFunc(); useEffect(() => { console.log(local); console.log(local); }, [local]); } ` }, { code: normalizeIndent` function MyComponent() { useEffect(() => { console.log(unresolved); }, []); } ` }, { code: normalizeIndent` function MyComponent() { const local = someFunc(); useEffect(() => { console.log(local); }, [,,,local,,,]); } ` }, { // Regression test code: normalizeIndent` function MyComponent({ foo }) { useEffect(() => { console.log(foo.length); }, [foo]); } ` }, { // Regression test code: normalizeIndent` function MyComponent({ foo }) { useEffect(() => { console.log(foo.length); console.log(foo.slice(0)); }, [foo]); } ` }, { // Regression test code: normalizeIndent` function MyComponent({ history }) { useEffect(() => { return history.listen(); }, [history]); } ` }, { // Valid because they have meaning without deps. code: normalizeIndent` function MyComponent(props) { useEffect(() => {}); useLayoutEffect(() => {}); useImperativeHandle(props.innerRef, () => {}); } ` }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); }, [props.foo]); } ` }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); console.log(props.bar); }, [props.bar, props.foo]); } ` }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); console.log(props.bar); }, [props.foo, props.bar]); } ` }, { code: normalizeIndent` function MyComponent(props) { const local = someFunc(); useEffect(() => { console.log(props.foo); console.log(props.bar); console.log(local); }, [props.foo, props.bar, local]); } ` }, { // [props, props.foo] is technically unnecessary ('props' covers 'props.foo'). // However, it's valid for effects to over-specify their deps. // So we don't warn about this. We *would* warn about useMemo/useCallback. code: normalizeIndent` function MyComponent(props) { const local = {}; useEffect(() => { console.log(props.foo); console.log(props.bar); }, [props, props.foo]); let color = someFunc(); useEffect(() => { console.log(props.foo.bar.baz); console.log(color); }, [props.foo, props.foo.bar.baz, color]); } ` }, // Nullish coalescing and optional chaining { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo?.bar?.baz ?? null); }, [props.foo]); } ` }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo?.bar); }, [props.foo?.bar]); } ` }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo?.bar); }, [props.foo.bar]); } ` }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo.bar); }, [props.foo?.bar]); } ` }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo.bar); console.log(props.foo?.bar); }, [props.foo?.bar]); } ` }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo.bar); console.log(props.foo?.bar); }, [props.foo.bar]); } ` }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); console.log(props.foo?.bar); }, [props.foo]); } ` }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo?.toString()); }, [props.foo]); } ` }, { code: normalizeIndent` function MyComponent(props) { useMemo(() => { console.log(props.foo?.toString()); }, [props.foo]); } ` }, { code: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo?.toString()); }, [props.foo]); } ` }, { code: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo.bar?.toString()); }, [props.foo.bar]); } ` }, { code: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo?.bar?.toString()); }, [props.foo.bar]); } ` }, { code: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo.bar.toString()); }, [props?.foo?.bar]); } ` }, { code: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo?.bar?.baz); }, [props?.foo.bar?.baz]); } ` }, { code: normalizeIndent` function MyComponent() { const myEffect = () => { // Doesn't use anything }; useEffect(myEffect, []); } ` }, { code: normalizeIndent` const local = {}; function MyComponent() { const myEffect = () => { console.log(local); }; useEffect(myEffect, []); } ` }, { code: normalizeIndent` const local = {}; function MyComponent() { function myEffect() { console.log(local); } useEffect(myEffect, []); } ` }, { code: normalizeIndent` function MyComponent() { const local = someFunc(); function myEffect() { console.log(local); } useEffect(myEffect, [local]); } ` }, { code: normalizeIndent` function MyComponent() { function myEffect() { console.log(global); } useEffect(myEffect, []); } ` }, { code: normalizeIndent` const local = {}; function MyComponent() { const myEffect = () => { otherThing() } const otherThing = () => { console.log(local); } useEffect(myEffect, []); } ` }, { // Valid because even though we don't inspect the function itself, // at least it's passed as a dependency. code: normalizeIndent` function MyComponent({delay}) { const local = {}; const myEffect = debounce(() => { console.log(local); }, delay); useEffect(myEffect, [myEffect]); } ` }, { code: normalizeIndent` function MyComponent({myEffect}) { useEffect(myEffect, [,myEffect]); } ` }, { code: normalizeIndent` function MyComponent({myEffect}) { useEffect(myEffect, [,myEffect,,]); } ` }, { code: normalizeIndent` let local = {}; function myEffect() { console.log(local); } function MyComponent() { useEffect(myEffect, []); } ` }, { code: normalizeIndent` function MyComponent({myEffect}) { useEffect(myEffect, [myEffect]); } ` }, { // Valid because has no deps. code: normalizeIndent` function MyComponent({myEffect}) { useEffect(myEffect); } ` }, { code: normalizeIndent` function MyComponent(props) { useCustomEffect(() => { console.log(props.foo); }); } `, options: [{ additionalHooks: 'useCustomEffect' }] }, { code: normalizeIndent` function MyComponent(props) { useCustomEffect(() => { console.log(props.foo); }, [props.foo]); } `, options: [{ additionalHooks: 'useCustomEffect' }] }, { code: normalizeIndent` function MyComponent(props) { useCustomEffect(() => { console.log(props.foo); }, []); } `, options: [{ additionalHooks: 'useAnotherEffect' }] }, { code: normalizeIndent` function MyComponent(props) { useWithoutEffectSuffix(() => { console.log(props.foo); }, []); } ` }, { code: normalizeIndent` function MyComponent(props) { return renderHelperConfusedWithEffect(() => { console.log(props.foo); }, []); } ` }, { // Valid because we don't care about hooks outside of components. code: normalizeIndent` const local = {}; useEffect(() => { console.log(local); }, []); ` }, { // Valid because we don't care about hooks outside of components. code: normalizeIndent` const local1 = {}; { const local2 = {}; useEffect(() => { console.log(local1); console.log(local2); }, []); } ` }, { code: normalizeIndent` function MyComponent() { const ref = useRef(); useEffect(() => { console.log(ref.current); }, [ref]); } ` }, { code: normalizeIndent` function MyComponent() { const ref = useRef(); useEffect(() => { console.log(ref.current); }, []); } ` }, { code: normalizeIndent` function MyComponent({ maybeRef2, foo }) { const definitelyRef1 = useRef(); const definitelyRef2 = useRef(); const maybeRef1 = useSomeOtherRefyThing(); const [state1, setState1] = useState(); const [state2, setState2] = React.useState(); const [state3, dispatch1] = useReducer(); const [state4, dispatch2] = React.useReducer(); const [state5, maybeSetState] = useFunnyState(); const [state6, maybeDispatch] = useFunnyReducer(); const [isPending1] = useTransition(); const [isPending2, startTransition2] = useTransition(); const [isPending3] = React.useTransition(); const [isPending4, startTransition4] = React.useTransition(); const mySetState = useCallback(() => {}, []); let myDispatch = useCallback(() => {}, []); useEffect(() => { // Known to be static console.log(definitelyRef1.current); console.log(definitelyRef2.current); console.log(maybeRef1.current); console.log(maybeRef2.current); setState1(); setState2(); dispatch1(); dispatch2(); startTransition1(); startTransition2(); startTransition3(); startTransition4(); // Dynamic console.log(state1); console.log(state2); console.log(state3); console.log(state4); console.log(state5); console.log(state6); console.log(isPending2); console.log(isPending4); mySetState(); myDispatch(); // Not sure; assume dynamic maybeSetState(); maybeDispatch(); }, [ // Dynamic state1, state2, state3, state4, state5, state6, maybeRef1, maybeRef2, isPending2, isPending4, // Not sure; assume dynamic mySetState, myDispatch, maybeSetState, maybeDispatch // In this test, we don't specify static deps. // That should be okay. ]); } ` }, { code: normalizeIndent` function MyComponent({ maybeRef2 }) { const definitelyRef1 = useRef(); const definitelyRef2 = useRef(); const maybeRef1 = useSomeOtherRefyThing(); const [state1, setState1] = useState(); const [state2, setState2] = React.useState(); const [state3, dispatch1] = useReducer(); const [state4, dispatch2] = React.useReducer(); const [state5, maybeSetState] = useFunnyState(); const [state6, maybeDispatch] = useFunnyReducer(); const mySetState = useCallback(() => {}, []); let myDispatch = useCallback(() => {}, []); useEffect(() => { // Known to be static console.log(definitelyRef1.current); console.log(definitelyRef2.current); console.log(maybeRef1.current); console.log(maybeRef2.current); setState1(); setState2(); dispatch1(); dispatch2(); // Dynamic console.log(state1); console.log(state2); console.log(state3); console.log(state4); console.log(state5); console.log(state6); mySetState(); myDispatch(); // Not sure; assume dynamic maybeSetState(); maybeDispatch(); }, [ // Dynamic state1, state2, state3, state4, state5, state6, maybeRef1, maybeRef2, // Not sure; assume dynamic mySetState, myDispatch, maybeSetState, maybeDispatch, // In this test, we specify static deps. // That should be okay too! definitelyRef1, definitelyRef2, setState1, setState2, dispatch1, dispatch2 ]); } ` }, { code: normalizeIndent` const MyComponent = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ focus() { alert(props.hello); } })) }); ` }, { code: normalizeIndent` const MyComponent = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ focus() { alert(props.hello); } }), [props.hello]) }); ` }, { // This is not ideal but warning would likely create // too many false positives. We do, however, prevent // direct assignments. code: normalizeIndent` function MyComponent(props) { let obj = someFunc(); useEffect(() => { obj.foo = true; }, [obj]); } ` }, { code: normalizeIndent` function MyComponent(props) { let foo = {} useEffect(() => { foo.bar.baz = 43; }, [foo.bar]); } ` }, { // Valid because we assign ref.current // ourselves. Therefore it's likely not // a ref managed by React. code: normalizeIndent` function MyComponent() { const myRef = useRef(); useEffect(() => { const handleMove = () => {}; myRef.current = {}; return () => { console.log(myRef.current.toString()) }; }, []); return
; } ` }, { // Valid because we assign ref.current // ourselves. Therefore it's likely not // a ref managed by React. code: normalizeIndent` function MyComponent() { const myRef = useRef(); useEffect(() => { const handleMove = () => {}; myRef.current = {}; return () => { console.log(myRef?.current?.toString()) }; }, []); return
; } ` }, { // Valid because we assign ref.current // ourselves. Therefore it's likely not // a ref managed by React. code: normalizeIndent` function useMyThing(myRef) { useEffect(() => { const handleMove = () => {}; myRef.current = {}; return () => { console.log(myRef.current.toString()) }; }, [myRef]); } ` }, { // Valid because the ref is captured. code: normalizeIndent` function MyComponent() { const myRef = useRef(); useEffect(() => { const handleMove = () => {}; const node = myRef.current; node.addEventListener('mousemove', handleMove); return () => node.removeEventListener('mousemove', handleMove); }, []); return
; } ` }, { // Valid because the ref is captured. code: normalizeIndent` function useMyThing(myRef) { useEffect(() => { const handleMove = () => {}; const node = myRef.current; node.addEventListener('mousemove', handleMove); return () => node.removeEventListener('mousemove', handleMove); }, [myRef]); return
; } ` }, { // Valid because it's not an effect. code: normalizeIndent` function useMyThing(myRef) { useCallback(() => { const handleMouse = () => {}; myRef.current.addEventListener('mousemove', handleMouse); myRef.current.addEventListener('mousein', handleMouse); return function() { setTimeout(() => { myRef.current.removeEventListener('mousemove', handleMouse); myRef.current.removeEventListener('mousein', handleMouse); }); } }, [myRef]); } ` }, { // Valid because we read ref.current in a function that isn't cleanup. code: normalizeIndent` function useMyThing() { const myRef = useRef(); useEffect(() => { const handleMove = () => { console.log(myRef.current) }; window.addEventListener('mousemove', handleMove); return () => window.removeEventListener('mousemove', handleMove); }, []); return
; } ` }, { // Valid because we read ref.current in a function that isn't cleanup. code: normalizeIndent` function useMyThing() { const myRef = useRef(); useEffect(() => { const handleMove = () => { return () => window.removeEventListener('mousemove', handleMove); }; window.addEventListener('mousemove', handleMove); return () => {}; }, []); return
; } ` }, { // Valid because it's a primitive constant. code: normalizeIndent` function MyComponent() { const local1 = 42; const local2 = '42'; const local3 = null; useEffect(() => { console.log(local1); console.log(local2); console.log(local3); }, []); } ` }, { // It's not a mistake to specify constant values though. code: normalizeIndent` function MyComponent() { const local1 = 42; const local2 = '42'; const local3 = null; useEffect(() => { console.log(local1); console.log(local2); console.log(local3); }, [local1, local2, local3]); } ` }, { // It is valid for effects to over-specify their deps. code: normalizeIndent` function MyComponent(props) { const local = props.local; useEffect(() => {}, [local]); } ` }, { // Valid even though activeTab is "unused". // We allow over-specifying deps for effects, but not callbacks or memo. code: normalizeIndent` function Foo({ activeTab }) { useEffect(() => { window.scrollTo(0, 0); }, [activeTab]); } ` }, { // It is valid to specify broader effect deps than strictly necessary. // Don't warn for this. code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo.bar.baz); }, [props]); useEffect(() => { console.log(props.foo.bar.baz); }, [props.foo]); useEffect(() => { console.log(props.foo.bar.baz); }, [props.foo.bar]); useEffect(() => { console.log(props.foo.bar.baz); }, [props.foo.bar.baz]); } ` }, { // It is *also* valid to specify broader memo/callback deps than strictly necessary. // Don't warn for this either. code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); }, [props]); const fn2 = useCallback(() => { console.log(props.foo.bar.baz); }, [props.foo]); const fn3 = useMemo(() => { console.log(props.foo.bar.baz); }, [props.foo.bar]); const fn4 = useMemo(() => { console.log(props.foo.bar.baz); }, [props.foo.bar.baz]); } ` }, { // Declaring handleNext is optional because // it doesn't use anything in the function scope. code: normalizeIndent` function MyComponent(props) { function handleNext1() { console.log('hello'); } const handleNext2 = () => { console.log('hello'); }; let handleNext3 = function() { console.log('hello'); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, []); } ` }, { // Declaring handleNext is optional because // it doesn't use anything in the function scope. code: normalizeIndent` function MyComponent(props) { function handleNext() { console.log('hello'); } useEffect(() => { return Store.subscribe(handleNext); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext); }, []); useMemo(() => { return Store.subscribe(handleNext); }, []); } ` }, { // Declaring handleNext is optional because // everything they use is fully static. code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); function handleNext1(value) { let value2 = value * 100; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(foo(value)); console.log('hello'); }; let handleNext3 = function(value) { console.log(value); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, []); } ` }, { code: normalizeIndent` function useInterval(callback, delay) { const savedCallback = useRef(); useEffect(() => { savedCallback.current = callback; }); useEffect(() => { function tick() { savedCallback.current(); } if (delay !== null) { let id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]); } ` }, { code: normalizeIndent` function Counter() { const [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } ` }, { code: normalizeIndent` function Counter(unstableProp) { let [count, setCount] = useState(0); setCount = unstableProp useEffect(() => { let id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, [setCount]); return

{count}

; } ` }, { code: normalizeIndent` function Counter() { const [count, setCount] = useState(0); function tick() { setCount(c => c + 1); } useEffect(() => { let id = setInterval(() => { tick(); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } ` }, { code: normalizeIndent` function Counter() { const [count, dispatch] = useReducer((state, action) => { if (action === 'inc') { return state + 1; } }, 0); useEffect(() => { let id = setInterval(() => { dispatch('inc'); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } ` }, { code: normalizeIndent` function Counter() { const [count, dispatch] = useReducer((state, action) => { if (action === 'inc') { return state + 1; } }, 0); const tick = () => { dispatch('inc'); }; useEffect(() => { let id = setInterval(tick, 1000); return () => clearInterval(id); }, []); return

{count}

; } ` }, { // Regression test for a crash code: normalizeIndent` function Podcasts() { useEffect(() => { setPodcasts([]); }, []); let [podcasts, setPodcasts] = useState(null); } ` }, { code: normalizeIndent` function withFetch(fetchPodcasts) { return function Podcasts({ id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { fetchPodcasts(id).then(setPodcasts); }, [id]); } } ` }, { code: normalizeIndent` function Podcasts({ id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { function doFetch({ fetchPodcasts }) { fetchPodcasts(id).then(setPodcasts); } doFetch({ fetchPodcasts: API.fetchPodcasts }); }, [id]); } ` }, { code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); function increment(x) { return x + 1; } useEffect(() => { let id = setInterval(() => { setCount(increment); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } ` }, { code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); function increment(x) { return x + 1; } useEffect(() => { let id = setInterval(() => { setCount(count => increment(count)); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } ` }, { code: normalizeIndent` import increment from './increment'; function Counter() { let [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count => count + increment); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } ` }, { code: normalizeIndent` function withStuff(increment) { return function Counter() { let [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count => count + increment); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } } ` }, { code: normalizeIndent` function App() { const [query, setQuery] = useState('react'); const [state, setState] = useState(null); useEffect(() => { let ignore = false; fetchSomething(); async function fetchSomething() { const result = await (await fetch('http://hn.algolia.com/api/v1/search?query=' + query)).json(); if (!ignore) setState(result); } return () => { ignore = true; }; }, [query]); return ( <> setQuery(e.target.value)} /> {JSON.stringify(state)} ); } ` }, { code: normalizeIndent` function Example() { const foo = useCallback(() => { foo(); }, []); } ` }, { code: normalizeIndent` function Example({ prop }) { const foo = useCallback(() => { if (prop) { foo(); } }, [prop]); } ` }, { code: normalizeIndent` function Hello() { const [state, setState] = useState(0); useEffect(() => { const handleResize = () => setState(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }); } ` }, // Ignore arguments keyword for arrow functions. { code: normalizeIndent` function Example() { useEffect(() => { arguments }, []) } ` }, { code: normalizeIndent` function Example() { useEffect(() => { const bar = () => { arguments; }; bar(); }, []) } ` }, // Regression test. { code: normalizeIndent` function Example(props) { useEffect(() => { let topHeight = 0; topHeight = props.upperViewHeight; }, [props.upperViewHeight]); } ` }, // Regression test. { code: normalizeIndent` function Example(props) { useEffect(() => { let topHeight = 0; topHeight = props?.upperViewHeight; }, [props?.upperViewHeight]); } ` }, // Regression test. { code: normalizeIndent` function Example(props) { useEffect(() => { let topHeight = 0; topHeight = props?.upperViewHeight; }, [props]); } ` }, { code: normalizeIndent` function useFoo(foo){ return useMemo(() => foo, [foo]); } ` }, { code: normalizeIndent` function useFoo(){ const foo = "hi!"; return useMemo(() => foo, [foo]); } ` }, { code: normalizeIndent` function useFoo(){ let {foo} = {foo: 1}; return useMemo(() => foo, [foo]); } ` }, { code: normalizeIndent` function useFoo(){ let [foo] = [1]; return useMemo(() => foo, [foo]); } ` }, { code: normalizeIndent` function useFoo() { const foo = "fine"; if (true) { // Shadowed variable with constant construction in a nested scope is fine. const foo = {}; } return useMemo(() => foo, [foo]); } ` }, { code: normalizeIndent` function MyComponent({foo}) { return useMemo(() => foo, [foo]) } ` }, { code: normalizeIndent` function MyComponent() { const foo = true ? "fine" : "also fine"; return useMemo(() => foo, [foo]); } ` }, { code: normalizeIndent` function MyComponent() { useEffect(() => { console.log('banana banana banana'); }, undefined); } ` }], invalid: [{ code: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo?.toString()); }, []); } `, errors: [{ message: "React Hook useCallback 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) { useCallback(() => { console.log(props.foo?.toString()); }, [props.foo]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo?.bar.baz); }, []); } `, errors: [{ message: "React Hook useCallback has a missing dependency: 'props.foo?.bar.baz'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.foo?.bar.baz]', output: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo?.bar.baz); }, [props.foo?.bar.baz]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo?.bar?.baz); }, []); } `, errors: [{ message: "React Hook useCallback has a missing dependency: 'props.foo?.bar?.baz'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.foo?.bar?.baz]', output: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo?.bar?.baz); }, [props.foo?.bar?.baz]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo?.bar.toString()); }, []); } `, errors: [{ message: "React Hook useCallback has a missing dependency: 'props.foo?.bar'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.foo?.bar]', output: normalizeIndent` function MyComponent(props) { useCallback(() => { console.log(props.foo?.bar.toString()); }, [props.foo?.bar]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = someFunc(); useEffect(() => { console.log(local); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = someFunc(); useEffect(() => { console.log(local); }, [local]); } ` }] }] }, { code: normalizeIndent` function Counter(unstableProp) { let [count, setCount] = useState(0); setCount = unstableProp useEffect(() => { let id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'setCount'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [setCount]', output: normalizeIndent` function Counter(unstableProp) { let [count, setCount] = useState(0); setCount = unstableProp useEffect(() => { let id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, [setCount]); return

{count}

; } ` }] }] }, { // Note: we *could* detect it's a primitive and never assigned // even though it's not a constant -- but we currently don't. // So this is an error. code: normalizeIndent` function MyComponent() { let local = 42; useEffect(() => { console.log(local); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { let local = 42; useEffect(() => { console.log(local); }, [local]); } ` }] }] }, { // Regexes are literals but potentially stateful. code: normalizeIndent` function MyComponent() { const local = /foo/; useEffect(() => { console.log(local); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = /foo/; useEffect(() => { console.log(local); }, [local]); } ` }] }] }, { // Invalid because they don't have a meaning without deps. code: normalizeIndent` function MyComponent(props) { const value = useMemo(() => { return 2*2; }); const fn = useCallback(() => { alert('foo'); }); } `, // We don't know what you meant. errors: [{ message: 'React Hook useMemo does nothing when called with only one argument. ' + 'Did you forget to pass an array of dependencies?', suggestions: undefined }, { message: 'React Hook useCallback does nothing when called with only one argument. ' + 'Did you forget to pass an array of dependencies?', suggestions: undefined }] }, { // Invalid because they don't have a meaning without deps. code: normalizeIndent` function MyComponent({ fn1, fn2 }) { const value = useMemo(fn1); const fn = useCallback(fn2); } `, errors: [{ message: 'React Hook useMemo does nothing when called with only one argument. ' + 'Did you forget to pass an array of dependencies?', suggestions: undefined }, { message: 'React Hook useCallback does nothing when called with only one argument. ' + 'Did you forget to pass an array of dependencies?', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent() { useEffect() useLayoutEffect() useCallback() useMemo() } `, errors: [{ message: 'React Hook useEffect requires an effect callback. ' + 'Did you forget to pass a callback to the hook?', suggestions: undefined }, { message: 'React Hook useLayoutEffect requires an effect callback. ' + 'Did you forget to pass a callback to the hook?', suggestions: undefined }, { message: 'React Hook useCallback requires an effect callback. ' + 'Did you forget to pass a callback to the hook?', suggestions: undefined }, { message: 'React Hook useMemo requires an effect callback. ' + 'Did you forget to pass a callback to the hook?', suggestions: undefined }] }, { // Regression test code: normalizeIndent` function MyComponent() { const local = someFunc(); useEffect(() => { if (true) { console.log(local); } }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = someFunc(); useEffect(() => { if (true) { console.log(local); } }, [local]); } ` }] }] }, { // Regression test code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { try { console.log(local); } finally {} }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { try { console.log(local); } finally {} }, [local]); } ` }] }] }, { // Regression test code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { function inner() { console.log(local); } inner(); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { function inner() { console.log(local); } inner(); }, [local]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local1 = someFunc(); { const local2 = someFunc(); useEffect(() => { console.log(local1); console.log(local2); }, []); } } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'local1' and 'local2'. " + 'Either include them or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local1, local2]', output: normalizeIndent` function MyComponent() { const local1 = someFunc(); { const local2 = someFunc(); useEffect(() => { console.log(local1); console.log(local2); }, [local1, local2]); } } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local1 = {}; const local2 = {}; useEffect(() => { console.log(local1); console.log(local2); }, [local1]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local2'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local1, local2]', output: normalizeIndent` function MyComponent() { const local1 = {}; const local2 = {}; useEffect(() => { console.log(local1); console.log(local2); }, [local1, local2]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local1 = {}; const local2 = {}; useMemo(() => { console.log(local1); }, [local1, local2]); } `, errors: [{ message: "React Hook useMemo has an unnecessary dependency: 'local2'. " + 'Either exclude it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local1]', output: normalizeIndent` function MyComponent() { const local1 = {}; const local2 = {}; useMemo(() => { console.log(local1); }, [local1]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local1 = someFunc(); function MyNestedComponent() { const local2 = {}; useCallback(() => { console.log(local1); console.log(local2); }, [local1]); } } `, errors: [{ message: "React Hook useCallback has a missing dependency: 'local2'. " + 'Either include it or remove the dependency array. ' + "Outer scope values like 'local1' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: [local2]', output: normalizeIndent` function MyComponent() { const local1 = someFunc(); function MyNestedComponent() { const local2 = {}; useCallback(() => { console.log(local1); console.log(local2); }, [local2]); } } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); console.log(local); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); console.log(local); }, [local]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); console.log(local); }, [local, local]); } `, errors: [{ message: "React Hook useEffect has a duplicate dependency: 'local'. " + 'Either omit it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); console.log(local); }, [local]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { useCallback(() => {}, [window]); } `, errors: [{ message: "React Hook useCallback has an unnecessary dependency: 'window'. " + 'Either exclude it or remove the dependency array. ' + "Outer scope values like 'window' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` function MyComponent() { useCallback(() => {}, []); } ` }] }] }, { // It is not valid for useCallback to specify extraneous deps // because it doesn't serve as a side effect trigger unlike useEffect. code: normalizeIndent` function MyComponent(props) { let local = props.foo; useCallback(() => {}, [local]); } `, errors: [{ message: "React Hook useCallback has an unnecessary dependency: 'local'. " + 'Either exclude it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` function MyComponent(props) { let local = props.foo; useCallback(() => {}, []); } ` }] }] }, { code: normalizeIndent` function MyComponent({ history }) { useEffect(() => { return history.listen(); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'history'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [history]', output: normalizeIndent` function MyComponent({ history }) { useEffect(() => { return history.listen(); }, [history]); } ` }] }] }, { code: normalizeIndent` function MyComponent({ history }) { useEffect(() => { return [ history.foo.bar[2].dobedo.listen(), history.foo.bar().dobedo.listen[2] ]; }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'history.foo'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [history.foo]', output: normalizeIndent` function MyComponent({ history }) { useEffect(() => { return [ history.foo.bar[2].dobedo.listen(), history.foo.bar().dobedo.listen[2] ]; }, [history.foo]); } ` }] }] }, { code: normalizeIndent` function MyComponent({ history }) { useEffect(() => { return [ history?.foo ]; }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'history?.foo'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [history?.foo]', output: normalizeIndent` function MyComponent({ history }) { useEffect(() => { return [ history?.foo ]; }, [history?.foo]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { useEffect(() => {}, ['foo']); } `, errors: [{ message: // Don't assume user meant `foo` because it's not used in the effect. "The 'foo' literal is not a valid dependency because it never changes. " + 'You can safely remove it.', // TODO: provide suggestion. suggestions: undefined }] }, { code: normalizeIndent` function MyComponent({ foo, bar, baz }) { useEffect(() => { console.log(foo, bar, baz); }, ['foo', 'bar']); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'bar', 'baz', and 'foo'. " + 'Either include them or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [bar, baz, foo]', output: normalizeIndent` function MyComponent({ foo, bar, baz }) { useEffect(() => { console.log(foo, bar, baz); }, [bar, baz, foo]); } ` }] }, { message: "The 'foo' literal is not a valid dependency because it never changes. " + 'Did you mean to include foo in the array instead?', suggestions: undefined }, { message: "The 'bar' literal is not a valid dependency because it never changes. " + 'Did you mean to include bar in the array instead?', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent({ foo, bar, baz }) { useEffect(() => { console.log(foo, bar, baz); }, [42, false, null]); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'bar', 'baz', and 'foo'. " + 'Either include them or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [bar, baz, foo]', output: normalizeIndent` function MyComponent({ foo, bar, baz }) { useEffect(() => { console.log(foo, bar, baz); }, [bar, baz, foo]); } ` }] }, { message: 'The 42 literal is not a valid dependency because it never changes. You can safely remove it.', suggestions: undefined }, { message: 'The false literal is not a valid dependency because it never changes. You can safely remove it.', suggestions: undefined }, { message: 'The null literal is not a valid dependency because it never changes. You can safely remove it.', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent() { const dependencies = []; useEffect(() => {}, dependencies); } `, errors: [{ message: 'React Hook useEffect was passed a dependency list that is not an ' + "array literal. This means we can't statically verify whether you've " + 'passed the correct dependencies.', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent() { const local = {}; const dependencies = [local]; useEffect(() => { console.log(local); }, dependencies); } `, errors: [{ message: 'React Hook useEffect was passed a dependency list that is not an ' + "array literal. This means we can't statically verify whether you've " + 'passed the correct dependencies.', // TODO: should this autofix or bail out? suggestions: undefined }, { message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; const dependencies = [local]; useEffect(() => { console.log(local); }, [local]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; const dependencies = [local]; useEffect(() => { console.log(local); }, [...dependencies]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; const dependencies = [local]; useEffect(() => { console.log(local); }, [local]); } ` }] }, { message: 'React Hook useEffect has a spread element in its dependency array. ' + "This means we can't statically verify whether you've passed the " + 'correct dependencies.', // TODO: should this autofix or bail out? suggestions: undefined }] }, { code: normalizeIndent` function MyComponent() { const local = someFunc(); useEffect(() => { console.log(local); }, [local, ...dependencies]); } `, errors: [{ message: 'React Hook useEffect has a spread element in its dependency array. ' + "This means we can't statically verify whether you've passed the " + 'correct dependencies.', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [computeCacheKey(local)]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', // TODO: I'm not sure this is a good idea. // Maybe bail out? suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [local]); } ` }] }, { message: 'React Hook useEffect has a complex expression in the dependency array. ' + 'Extract it to a separate variable so it can be statically checked.', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.items[0]); }, [props.items[0]]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'props.items'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.items]', output: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.items[0]); }, [props.items]); } ` }] }, { message: 'React Hook useEffect has a complex expression in the dependency array. ' + 'Extract it to a separate variable so it can be statically checked.', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.items[0]); }, [props.items, props.items[0]]); } `, errors: [{ message: 'React Hook useEffect has a complex expression in the dependency array. ' + 'Extract it to a separate variable so it can be statically checked.', // TODO: ideally suggestion would remove the bad expression? suggestions: undefined }] }, { code: normalizeIndent` function MyComponent({ items }) { useEffect(() => { console.log(items[0]); }, [items[0]]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'items'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [items]', output: normalizeIndent` function MyComponent({ items }) { useEffect(() => { console.log(items[0]); }, [items]); } ` }] }, { message: 'React Hook useEffect has a complex expression in the dependency array. ' + 'Extract it to a separate variable so it can be statically checked.', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent({ items }) { useEffect(() => { console.log(items[0]); }, [items, items[0]]); } `, errors: [{ message: 'React Hook useEffect has a complex expression in the dependency array. ' + 'Extract it to a separate variable so it can be statically checked.', // TODO: ideally suggeston would remove the bad expression? suggestions: undefined }] }, { // It is not valid for useCallback to specify extraneous deps // because it doesn't serve as a side effect trigger unlike useEffect. // However, we generally allow specifying *broader* deps as escape hatch. // So while [props, props.foo] is unnecessary, 'props' wins here as the // broader one, and this is why 'props.foo' is reported as unnecessary. code: normalizeIndent` function MyComponent(props) { const local = {}; useCallback(() => { console.log(props.foo); console.log(props.bar); }, [props, props.foo]); } `, errors: [{ message: "React Hook useCallback has an unnecessary dependency: 'props.foo'. " + 'Either exclude it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props]', output: normalizeIndent` function MyComponent(props) { const local = {}; useCallback(() => { console.log(props.foo); console.log(props.bar); }, [props]); } ` }] }] }, { // Since we don't have 'props' in the list, we'll suggest narrow dependencies. code: normalizeIndent` function MyComponent(props) { const local = {}; useCallback(() => { console.log(props.foo); console.log(props.bar); }, []); } `, errors: [{ message: "React Hook useCallback has missing dependencies: 'props.bar' and 'props.foo'. " + 'Either include them or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.bar, props.foo]', output: normalizeIndent` function MyComponent(props) { const local = {}; useCallback(() => { console.log(props.foo); console.log(props.bar); }, [props.bar, props.foo]); } ` }] }] }, { // Effects are allowed to over-specify deps. We'll complain about missing // 'local', but we won't remove the already-specified 'local.id' from your list. code: normalizeIndent` function MyComponent() { const local = {id: 42}; useEffect(() => { console.log(local); }, [local.id]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local, local.id]', output: normalizeIndent` function MyComponent() { const local = {id: 42}; useEffect(() => { console.log(local); }, [local, local.id]); } ` }] }] }, { // Callbacks are not allowed to over-specify deps. So we'll complain about missing // 'local' and we will also *remove* 'local.id' from your list. code: normalizeIndent` function MyComponent() { const local = {id: 42}; const fn = useCallback(() => { console.log(local); }, [local.id]); } `, errors: [{ message: "React Hook useCallback has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {id: 42}; const fn = useCallback(() => { console.log(local); }, [local]); } ` }] }] }, { // Callbacks are not allowed to over-specify deps. So we'll complain about // the unnecessary 'local.id'. code: normalizeIndent` function MyComponent() { const local = {id: 42}; const fn = useCallback(() => { console.log(local); }, [local.id, local]); } `, errors: [{ message: "React Hook useCallback has an unnecessary dependency: 'local.id'. " + 'Either exclude it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {id: 42}; const fn = useCallback(() => { console.log(local); }, [local]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); }, []); } `, errors: [{ message: "React Hook useCallback has a missing dependency: 'props.foo.bar.baz'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.foo.bar.baz]', output: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); }, [props.foo.bar.baz]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { let color = {} const fn = useCallback(() => { console.log(props.foo.bar.baz); console.log(color); }, [props.foo, props.foo.bar.baz]); } `, errors: [{ message: "React Hook useCallback has a missing dependency: 'color'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [color, props.foo.bar.baz]', output: normalizeIndent` function MyComponent(props) { let color = {} const fn = useCallback(() => { console.log(props.foo.bar.baz); console.log(color); }, [color, props.foo.bar.baz]); } ` }] }] }, { // Callbacks are not allowed to over-specify deps. So one of these is extra. // However, it *is* allowed to specify broader deps then strictly necessary. // So in this case we ask you to remove 'props.foo.bar.baz' because 'props.foo' // already covers it, and having both is unnecessary. // TODO: maybe consider suggesting a narrower one by default in these cases. code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); }, [props.foo.bar.baz, props.foo]); } `, errors: [{ message: "React Hook useCallback has an unnecessary dependency: 'props.foo.bar.baz'. " + 'Either exclude it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.foo]', output: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); }, [props.foo]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); console.log(props.foo.fizz.bizz); }, []); } `, errors: [{ message: "React Hook useCallback has missing dependencies: 'props.foo.bar.baz' and 'props.foo.fizz.bizz'. " + 'Either include them or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.foo.bar.baz, props.foo.fizz.bizz]', output: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); console.log(props.foo.fizz.bizz); }, [props.foo.bar.baz, props.foo.fizz.bizz]); } ` }] }] }, { // Normally we allow specifying deps too broadly. // So we'd be okay if 'props.foo.bar' was there rather than 'props.foo.bar.baz'. // However, 'props.foo.bar.baz' is missing. So we know there is a mistake. // When we're sure there is a mistake, for callbacks we will rebuild the list // from scratch. This will set the user on a better path by default. // This is why we end up with just 'props.foo.bar', and not them both. code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar); }, [props.foo.bar.baz]); } `, errors: [{ message: "React Hook useCallback has a missing dependency: 'props.foo.bar'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.foo.bar]', output: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar); }, [props.foo.bar]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props); console.log(props.hello); }, [props.foo.bar.baz]); } `, errors: [{ message: "React Hook useCallback has a missing dependency: 'props'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props]', output: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props); console.log(props.hello); }, [props]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [local, local]); } `, errors: [{ message: "React Hook useEffect has a duplicate dependency: 'local'. " + 'Either omit it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [local]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local1 = {}; useCallback(() => { const local1 = {}; console.log(local1); }, [local1]); } `, errors: [{ message: "React Hook useCallback has an unnecessary dependency: 'local1'. " + 'Either exclude it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` function MyComponent() { const local1 = {}; useCallback(() => { const local1 = {}; console.log(local1); }, []); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local1 = {}; useCallback(() => {}, [local1]); } `, errors: [{ message: "React Hook useCallback has an unnecessary dependency: 'local1'. " + 'Either exclude it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` function MyComponent() { const local1 = {}; useCallback(() => {}, []); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); }, []); } `, errors: [{ 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) { useEffect(() => { console.log(props.foo); }, [props.foo]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); console.log(props.bar); }, []); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'props.bar' and 'props.foo'. " + 'Either include them or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.bar, props.foo]', output: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); console.log(props.bar); }, [props.bar, props.foo]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { let a, b, c, d, e, f, g; useEffect(() => { console.log(b, e, d, c, a, g, f); }, [c, a, g]); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'b', 'd', 'e', and 'f'. " + 'Either include them or remove the dependency array.', // Don't alphabetize if it wasn't alphabetized in the first place. suggestions: [{ desc: 'Update the dependencies array to be: [c, a, g, b, e, d, f]', output: normalizeIndent` function MyComponent(props) { let a, b, c, d, e, f, g; useEffect(() => { console.log(b, e, d, c, a, g, f); }, [c, a, g, b, e, d, f]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { let a, b, c, d, e, f, g; useEffect(() => { console.log(b, e, d, c, a, g, f); }, [a, c, g]); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'b', 'd', 'e', and 'f'. " + 'Either include them or remove the dependency array.', // Alphabetize if it was alphabetized. suggestions: [{ desc: 'Update the dependencies array to be: [a, b, c, d, e, f, g]', output: normalizeIndent` function MyComponent(props) { let a, b, c, d, e, f, g; useEffect(() => { console.log(b, e, d, c, a, g, f); }, [a, b, c, d, e, f, g]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { let a, b, c, d, e, f, g; useEffect(() => { console.log(b, e, d, c, a, g, f); }, []); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'a', 'b', 'c', 'd', 'e', 'f', and 'g'. " + 'Either include them or remove the dependency array.', // Alphabetize if it was empty. suggestions: [{ desc: 'Update the dependencies array to be: [a, b, c, d, e, f, g]', output: normalizeIndent` function MyComponent(props) { let a, b, c, d, e, f, g; useEffect(() => { console.log(b, e, d, c, a, g, f); }, [a, b, c, d, e, f, g]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const local = {}; useEffect(() => { console.log(props.foo); console.log(props.bar); console.log(local); }, []); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'local', 'props.bar', and 'props.foo'. " + 'Either include them or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local, props.bar, props.foo]', output: normalizeIndent` function MyComponent(props) { const local = {}; useEffect(() => { console.log(props.foo); console.log(props.bar); console.log(local); }, [local, props.bar, props.foo]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const local = {}; useEffect(() => { console.log(props.foo); console.log(props.bar); console.log(local); }, [props]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local, props]', output: normalizeIndent` function MyComponent(props) { const local = {}; useEffect(() => { console.log(props.foo); console.log(props.bar); console.log(local); }, [local, props]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); }, []); useCallback(() => { console.log(props.foo); }, []); useMemo(() => { console.log(props.foo); }, []); React.useEffect(() => { console.log(props.foo); }, []); React.useCallback(() => { console.log(props.foo); }, []); React.useMemo(() => { console.log(props.foo); }, []); React.notReactiveHook(() => { console.log(props.foo); }, []); } `, errors: [{ 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) { useEffect(() => { console.log(props.foo); }, [props.foo]); useCallback(() => { console.log(props.foo); }, []); useMemo(() => { console.log(props.foo); }, []); React.useEffect(() => { console.log(props.foo); }, []); React.useCallback(() => { console.log(props.foo); }, []); React.useMemo(() => { console.log(props.foo); }, []); React.notReactiveHook(() => { console.log(props.foo); }, []); } ` }] }, { message: "React Hook useCallback 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) { useEffect(() => { console.log(props.foo); }, []); useCallback(() => { console.log(props.foo); }, [props.foo]); useMemo(() => { console.log(props.foo); }, []); React.useEffect(() => { console.log(props.foo); }, []); React.useCallback(() => { console.log(props.foo); }, []); React.useMemo(() => { console.log(props.foo); }, []); React.notReactiveHook(() => { console.log(props.foo); }, []); } ` }] }, { message: "React Hook useMemo 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) { useEffect(() => { console.log(props.foo); }, []); useCallback(() => { console.log(props.foo); }, []); useMemo(() => { console.log(props.foo); }, [props.foo]); React.useEffect(() => { console.log(props.foo); }, []); React.useCallback(() => { console.log(props.foo); }, []); React.useMemo(() => { console.log(props.foo); }, []); React.notReactiveHook(() => { 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) { useEffect(() => { console.log(props.foo); }, []); useCallback(() => { console.log(props.foo); }, []); useMemo(() => { console.log(props.foo); }, []); React.useEffect(() => { console.log(props.foo); }, [props.foo]); React.useCallback(() => { console.log(props.foo); }, []); React.useMemo(() => { console.log(props.foo); }, []); React.notReactiveHook(() => { console.log(props.foo); }, []); } ` }] }, { message: "React Hook React.useCallback 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) { useEffect(() => { console.log(props.foo); }, []); useCallback(() => { console.log(props.foo); }, []); useMemo(() => { console.log(props.foo); }, []); React.useEffect(() => { console.log(props.foo); }, []); React.useCallback(() => { console.log(props.foo); }, [props.foo]); React.useMemo(() => { console.log(props.foo); }, []); React.notReactiveHook(() => { console.log(props.foo); }, []); } ` }] }, { message: "React Hook React.useMemo 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) { useEffect(() => { console.log(props.foo); }, []); useCallback(() => { console.log(props.foo); }, []); useMemo(() => { console.log(props.foo); }, []); React.useEffect(() => { console.log(props.foo); }, []); React.useCallback(() => { console.log(props.foo); }, []); React.useMemo(() => { console.log(props.foo); }, [props.foo]); React.notReactiveHook(() => { console.log(props.foo); }, []); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useCustomEffect(() => { console.log(props.foo); }, []); useEffect(() => { console.log(props.foo); }, []); React.useEffect(() => { console.log(props.foo); }, []); React.useCustomEffect(() => { console.log(props.foo); }, []); } `, options: [{ additionalHooks: 'useCustomEffect' }], errors: [{ message: "React Hook useCustomEffect 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) { useCustomEffect(() => { console.log(props.foo); }, [props.foo]); useEffect(() => { console.log(props.foo); }, []); React.useEffect(() => { console.log(props.foo); }, []); React.useCustomEffect(() => { 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) { useCustomEffect(() => { console.log(props.foo); }, []); useEffect(() => { console.log(props.foo); }, [props.foo]); React.useEffect(() => { console.log(props.foo); }, []); React.useCustomEffect(() => { 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) { useCustomEffect(() => { console.log(props.foo); }, []); useEffect(() => { console.log(props.foo); }, []); React.useEffect(() => { console.log(props.foo); }, [props.foo]); React.useCustomEffect(() => { console.log(props.foo); }, []); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [a ? local : b]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', // TODO: should we bail out instead? suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [local]); } ` }] }, { message: 'React Hook useEffect has a complex expression in the dependency array. ' + 'Extract it to a separate variable so it can be statically checked.', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [a && local]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', // TODO: should we bail out instead? suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [local]); } ` }] }, { message: 'React Hook useEffect has a complex expression in the dependency array. ' + 'Extract it to a separate variable so it can be statically checked.', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => {}, [props?.attribute.method()]); } `, errors: [{ message: 'React Hook useEffect has a complex expression in the dependency array. ' + 'Extract it to a separate variable so it can be statically checked.', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => {}, [props.method()]); } `, errors: [{ message: 'React Hook useEffect has a complex expression in the dependency array. ' + 'Extract it to a separate variable so it can be statically checked.', suggestions: undefined }] }, { code: normalizeIndent` function MyComponent() { const ref = useRef(); const [state, setState] = useState(); useEffect(() => { ref.current = {}; setState(state + 1); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'state'. " + 'Either include it or remove the dependency array. ' + `You can also do a functional update 'setState(s => ...)' ` + `if you only need 'state' in the 'setState' call.`, suggestions: [{ desc: 'Update the dependencies array to be: [state]', output: normalizeIndent` function MyComponent() { const ref = useRef(); const [state, setState] = useState(); useEffect(() => { ref.current = {}; setState(state + 1); }, [state]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const ref = useRef(); const [state, setState] = useState(); useEffect(() => { ref.current = {}; setState(state + 1); }, [ref]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'state'. " + 'Either include it or remove the dependency array. ' + `You can also do a functional update 'setState(s => ...)' ` + `if you only need 'state' in the 'setState' call.`, // We don't ask to remove static deps but don't add them either. // Don't suggest removing "ref" (it's fine either way) // but *do* add "state". *Don't* add "setState" ourselves. suggestions: [{ desc: 'Update the dependencies array to be: [ref, state]', output: normalizeIndent` function MyComponent() { const ref = useRef(); const [state, setState] = useState(); useEffect(() => { ref.current = {}; setState(state + 1); }, [ref, state]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const ref1 = useRef(); const ref2 = useRef(); useEffect(() => { ref1.current.focus(); console.log(ref2.current.textContent); alert(props.someOtherRefs.current.innerHTML); fetch(props.color); }, []); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'props.color' and 'props.someOtherRefs'. " + 'Either include them or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.color, props.someOtherRefs]', output: normalizeIndent` function MyComponent(props) { const ref1 = useRef(); const ref2 = useRef(); useEffect(() => { ref1.current.focus(); console.log(ref2.current.textContent); alert(props.someOtherRefs.current.innerHTML); fetch(props.color); }, [props.color, props.someOtherRefs]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const ref1 = useRef(); const ref2 = useRef(); useEffect(() => { ref1.current.focus(); console.log(ref2.current.textContent); alert(props.someOtherRefs.current.innerHTML); fetch(props.color); }, [ref1.current, ref2.current, props.someOtherRefs, props.color]); } `, errors: [{ message: "React Hook useEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + 'Either exclude them or remove the dependency array. ' + "Mutable values like 'ref1.current' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: [props.someOtherRefs, props.color]', output: normalizeIndent` function MyComponent(props) { const ref1 = useRef(); const ref2 = useRef(); useEffect(() => { ref1.current.focus(); console.log(ref2.current.textContent); alert(props.someOtherRefs.current.innerHTML); fetch(props.color); }, [props.someOtherRefs, props.color]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const ref1 = useRef(); const ref2 = useRef(); useEffect(() => { ref1?.current?.focus(); console.log(ref2?.current?.textContent); alert(props.someOtherRefs.current.innerHTML); fetch(props.color); }, [ref1?.current, ref2?.current, props.someOtherRefs, props.color]); } `, errors: [{ message: "React Hook useEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + 'Either exclude them or remove the dependency array. ' + "Mutable values like 'ref1.current' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: [props.someOtherRefs, props.color]', output: normalizeIndent` function MyComponent(props) { const ref1 = useRef(); const ref2 = useRef(); useEffect(() => { ref1?.current?.focus(); console.log(ref2?.current?.textContent); alert(props.someOtherRefs.current.innerHTML); fetch(props.color); }, [props.someOtherRefs, props.color]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const ref = useRef(); useEffect(() => { console.log(ref.current); }, [ref.current]); } `, errors: [{ message: "React Hook useEffect has an unnecessary dependency: 'ref.current'. " + 'Either exclude it or remove the dependency array. ' + "Mutable values like 'ref.current' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` function MyComponent() { const ref = useRef(); useEffect(() => { console.log(ref.current); }, []); } ` }] }] }, { code: normalizeIndent` function MyComponent({ activeTab }) { const ref1 = useRef(); const ref2 = useRef(); useEffect(() => { ref1.current.scrollTop = 0; ref2.current.scrollTop = 0; }, [ref1.current, ref2.current, activeTab]); } `, errors: [{ message: "React Hook useEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + 'Either exclude them or remove the dependency array. ' + "Mutable values like 'ref1.current' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: [activeTab]', output: normalizeIndent` function MyComponent({ activeTab }) { const ref1 = useRef(); const ref2 = useRef(); useEffect(() => { ref1.current.scrollTop = 0; ref2.current.scrollTop = 0; }, [activeTab]); } ` }] }] }, { code: normalizeIndent` function MyComponent({ activeTab, initY }) { const ref1 = useRef(); const ref2 = useRef(); const fn = useCallback(() => { ref1.current.scrollTop = initY; ref2.current.scrollTop = initY; }, [ref1.current, ref2.current, activeTab, initY]); } `, errors: [{ message: "React Hook useCallback has unnecessary dependencies: 'activeTab', 'ref1.current', and 'ref2.current'. " + 'Either exclude them or remove the dependency array. ' + "Mutable values like 'ref1.current' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: [initY]', output: normalizeIndent` function MyComponent({ activeTab, initY }) { const ref1 = useRef(); const ref2 = useRef(); const fn = useCallback(() => { ref1.current.scrollTop = initY; ref2.current.scrollTop = initY; }, [initY]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const ref = useRef(); useEffect(() => { console.log(ref.current); }, [ref.current, ref]); } `, errors: [{ message: "React Hook useEffect has an unnecessary dependency: 'ref.current'. " + 'Either exclude it or remove the dependency array. ' + "Mutable values like 'ref.current' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: [ref]', output: normalizeIndent` function MyComponent() { const ref = useRef(); useEffect(() => { console.log(ref.current); }, [ref]); } ` }] }] }, { code: normalizeIndent` const MyComponent = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ focus() { alert(props.hello); } }), []) }); `, errors: [{ message: "React Hook useImperativeHandle has a missing dependency: 'props.hello'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.hello]', output: normalizeIndent` const MyComponent = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ focus() { alert(props.hello); } }), [props.hello]) }); ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { if (props.onChange) { props.onChange(); } }, []); } `, errors: [{ message: "React Hook useEffect 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 useEffect call and refer to those specific ` + `props inside useEffect.`, suggestions: [{ desc: 'Update the dependencies array to be: [props]', output: normalizeIndent` function MyComponent(props) { useEffect(() => { if (props.onChange) { props.onChange(); } }, [props]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { if (props?.onChange) { props?.onChange(); } }, []); } `, errors: [{ message: "React Hook useEffect 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 useEffect call and refer to those specific ` + `props inside useEffect.`, suggestions: [{ desc: 'Update the dependencies array to be: [props]', output: normalizeIndent` function MyComponent(props) { useEffect(() => { if (props?.onChange) { props?.onChange(); } }, [props]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { function play() { props.onPlay(); } function pause() { props.onPause(); } }, []); } `, errors: [{ message: "React Hook useEffect 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 useEffect call and refer to those specific ` + `props inside useEffect.`, suggestions: [{ desc: 'Update the dependencies array to be: [props]', output: normalizeIndent` function MyComponent(props) { useEffect(() => { function play() { props.onPlay(); } function pause() { props.onPause(); } }, [props]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { if (props.foo.onChange) { props.foo.onChange(); } }, []); } `, errors: [{ 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) { useEffect(() => { if (props.foo.onChange) { props.foo.onChange(); } }, [props.foo]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { props.onChange(); if (props.foo.onChange) { props.foo.onChange(); } }, []); } `, errors: [{ message: "React Hook useEffect 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 useEffect call and refer to those specific ` + `props inside useEffect.`, suggestions: [{ desc: 'Update the dependencies array to be: [props]', output: normalizeIndent` function MyComponent(props) { useEffect(() => { props.onChange(); if (props.foo.onChange) { props.foo.onChange(); } }, [props]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const [skillsCount] = useState(); useEffect(() => { if (skillsCount === 0 && !props.isEditMode) { props.toggleEditMode(); } }, [skillsCount, props.isEditMode, props.toggleEditMode]); } `, errors: [{ message: "React Hook useEffect 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 useEffect call and refer to those specific ` + `props inside useEffect.`, suggestions: [{ desc: 'Update the dependencies array to be: [skillsCount, props.isEditMode, props.toggleEditMode, props]', output: normalizeIndent` function MyComponent(props) { const [skillsCount] = useState(); useEffect(() => { if (skillsCount === 0 && !props.isEditMode) { props.toggleEditMode(); } }, [skillsCount, props.isEditMode, props.toggleEditMode, props]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const [skillsCount] = useState(); useEffect(() => { if (skillsCount === 0 && !props.isEditMode) { props.toggleEditMode(); } }, []); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'props' and 'skillsCount'. " + 'Either include them 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 useEffect call and refer to those specific ` + `props inside useEffect.`, suggestions: [{ desc: 'Update the dependencies array to be: [props, skillsCount]', output: normalizeIndent` function MyComponent(props) { const [skillsCount] = useState(); useEffect(() => { if (skillsCount === 0 && !props.isEditMode) { props.toggleEditMode(); } }, [props, skillsCount]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { externalCall(props); props.onChange(); }, []); } `, // Don't suggest to destructure props here since you can't. errors: [{ message: "React Hook useEffect has a missing dependency: 'props'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props]', output: normalizeIndent` function MyComponent(props) { useEffect(() => { externalCall(props); props.onChange(); }, [props]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { useEffect(() => { props.onChange(); externalCall(props); }, []); } `, // Don't suggest to destructure props here since you can't. errors: [{ message: "React Hook useEffect has a missing dependency: 'props'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props]', output: normalizeIndent` function MyComponent(props) { useEffect(() => { props.onChange(); externalCall(props); }, [props]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { let value; let value2; let value3; let value4; let asyncValue; useEffect(() => { if (value4) { value = {}; } value2 = 100; value = 43; value4 = true; console.log(value2); console.log(value3); setTimeout(() => { asyncValue = 100; }); }, []); } `, // This is a separate warning unrelated to others. // We could've made a separate rule for it but it's rare enough to name it. // No suggestions because the intent isn't clear. errors: [{ message: // value2 `Assignments to the 'value2' variable from inside React Hook useEffect ` + `will be lost after each render. To preserve the value over time, ` + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + `Otherwise, you can move this variable directly inside useEffect.`, suggestions: undefined }, { message: // value `Assignments to the 'value' variable from inside React Hook useEffect ` + `will be lost after each render. To preserve the value over time, ` + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + `Otherwise, you can move this variable directly inside useEffect.`, suggestions: undefined }, { message: // value4 `Assignments to the 'value4' variable from inside React Hook useEffect ` + `will be lost after each render. To preserve the value over time, ` + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + `Otherwise, you can move this variable directly inside useEffect.`, suggestions: undefined }, { message: // asyncValue `Assignments to the 'asyncValue' variable from inside React Hook useEffect ` + `will be lost after each render. To preserve the value over time, ` + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + `Otherwise, you can move this variable directly inside useEffect.`, suggestions: undefined }] }, { code: normalizeIndent` function MyComponent(props) { let value; let value2; let value3; let asyncValue; useEffect(() => { value = {}; value2 = 100; value = 43; console.log(value2); console.log(value3); setTimeout(() => { asyncValue = 100; }); }, [value, value2, value3]); } `, // This is a separate warning unrelated to others. // We could've made a separate rule for it but it's rare enough to name it. // No suggestions because the intent isn't clear. errors: [{ message: // value `Assignments to the 'value' variable from inside React Hook useEffect ` + `will be lost after each render. To preserve the value over time, ` + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + `Otherwise, you can move this variable directly inside useEffect.`, suggestions: undefined }, { message: // value2 `Assignments to the 'value2' variable from inside React Hook useEffect ` + `will be lost after each render. To preserve the value over time, ` + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + `Otherwise, you can move this variable directly inside useEffect.`, suggestions: undefined }, { message: // asyncValue `Assignments to the 'asyncValue' variable from inside React Hook useEffect ` + `will be lost after each render. To preserve the value over time, ` + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + `Otherwise, you can move this variable directly inside useEffect.`, suggestions: undefined }] }, { code: normalizeIndent` function MyComponent() { const myRef = useRef(); useEffect(() => { const handleMove = () => {}; myRef.current.addEventListener('mousemove', handleMove); return () => myRef.current.removeEventListener('mousemove', handleMove); }, []); return
; } `, errors: [{ message: `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.`, suggestions: undefined }] }, { code: normalizeIndent` function MyComponent() { const myRef = useRef(); useEffect(() => { const handleMove = () => {}; myRef?.current?.addEventListener('mousemove', handleMove); return () => myRef?.current?.removeEventListener('mousemove', handleMove); }, []); return
; } `, errors: [{ message: `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.`, suggestions: undefined }] }, { code: normalizeIndent` function MyComponent() { const myRef = useRef(); useEffect(() => { const handleMove = () => {}; myRef.current.addEventListener('mousemove', handleMove); return () => myRef.current.removeEventListener('mousemove', handleMove); }); return
; } `, errors: [{ message: `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.`, suggestions: undefined }] }, { code: normalizeIndent` function useMyThing(myRef) { useEffect(() => { const handleMove = () => {}; myRef.current.addEventListener('mousemove', handleMove); return () => myRef.current.removeEventListener('mousemove', handleMove); }, [myRef]); } `, errors: [{ message: `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.`, suggestions: undefined }] }, { code: normalizeIndent` function useMyThing(myRef) { useEffect(() => { const handleMouse = () => {}; myRef.current.addEventListener('mousemove', handleMouse); myRef.current.addEventListener('mousein', handleMouse); return function() { setTimeout(() => { myRef.current.removeEventListener('mousemove', handleMouse); myRef.current.removeEventListener('mousein', handleMouse); }); } }, [myRef]); } `, errors: [{ message: `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.`, suggestions: undefined }] }, { code: normalizeIndent` function useMyThing(myRef, active) { useEffect(() => { const handleMove = () => {}; if (active) { myRef.current.addEventListener('mousemove', handleMove); return function() { setTimeout(() => { myRef.current.removeEventListener('mousemove', handleMove); }); } } }, [myRef, active]); } `, errors: [{ message: `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.`, suggestions: undefined }] }, { code: ` function MyComponent() { const myRef = useRef(); useLayoutEffect_SAFE_FOR_SSR(() => { const handleMove = () => {}; myRef.current.addEventListener('mousemove', handleMove); return () => myRef.current.removeEventListener('mousemove', handleMove); }); return
; } `, // No changes output: null, 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.`], options: [{ additionalHooks: 'useLayoutEffect_SAFE_FOR_SSR' }] }, { // Autofix ignores constant primitives (leaving the ones that are there). code: normalizeIndent` function MyComponent() { const local1 = 42; const local2 = '42'; const local3 = null; const local4 = {}; useEffect(() => { console.log(local1); console.log(local2); console.log(local3); console.log(local4); }, [local1, local3]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local4'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local1, local3, local4]', output: normalizeIndent` function MyComponent() { const local1 = 42; const local2 = '42'; const local3 = null; const local4 = {}; useEffect(() => { console.log(local1); console.log(local2); console.log(local3); console.log(local4); }, [local1, local3, local4]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { useEffect(() => { window.scrollTo(0, 0); }, [window]); } `, errors: [{ message: "React Hook useEffect has an unnecessary dependency: 'window'. " + 'Either exclude it or remove the dependency array. ' + "Outer scope values like 'window' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` function MyComponent() { useEffect(() => { window.scrollTo(0, 0); }, []); } ` }] }] }, { code: normalizeIndent` import MutableStore from 'store'; function MyComponent() { useEffect(() => { console.log(MutableStore.hello); }, [MutableStore.hello]); } `, errors: [{ message: "React Hook useEffect has an unnecessary dependency: 'MutableStore.hello'. " + 'Either exclude it or remove the dependency array. ' + "Outer scope values like 'MutableStore.hello' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` import MutableStore from 'store'; function MyComponent() { useEffect(() => { console.log(MutableStore.hello); }, []); } ` }] }] }, { code: normalizeIndent` import MutableStore from 'store'; let z = {}; function MyComponent(props) { let x = props.foo; { let y = props.bar; useEffect(() => { console.log(MutableStore.hello.world, props.foo, x, y, z, global.stuff); }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]); } } `, errors: [{ message: 'React Hook useEffect has unnecessary dependencies: ' + "'MutableStore.hello.world', 'global.stuff', and 'z'. " + 'Either exclude them or remove the dependency array. ' + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: [props.foo, x, y]', output: normalizeIndent` import MutableStore from 'store'; let z = {}; function MyComponent(props) { let x = props.foo; { let y = props.bar; useEffect(() => { console.log(MutableStore.hello.world, props.foo, x, y, z, global.stuff); }, [props.foo, x, y]); } } ` }] }] }, { code: normalizeIndent` import MutableStore from 'store'; let z = {}; function MyComponent(props) { let x = props.foo; { let y = props.bar; useEffect(() => { // nothing }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]); } } `, errors: [{ message: 'React Hook useEffect has unnecessary dependencies: ' + "'MutableStore.hello.world', 'global.stuff', and 'z'. " + 'Either exclude them or remove the dependency array. ' + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + "because mutating them doesn't re-render the component.", // The output should contain the ones that are inside a component // since there are legit reasons to over-specify them for effects. suggestions: [{ desc: 'Update the dependencies array to be: [props.foo, x, y]', output: normalizeIndent` import MutableStore from 'store'; let z = {}; function MyComponent(props) { let x = props.foo; { let y = props.bar; useEffect(() => { // nothing }, [props.foo, x, y]); } } ` }] }] }, { code: normalizeIndent` import MutableStore from 'store'; let z = {}; function MyComponent(props) { let x = props.foo; { let y = props.bar; const fn = useCallback(() => { // nothing }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]); } } `, errors: [{ message: 'React Hook useCallback has unnecessary dependencies: ' + "'MutableStore.hello.world', 'global.stuff', 'props.foo', 'x', 'y', and 'z'. " + 'Either exclude them or remove the dependency array. ' + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` import MutableStore from 'store'; let z = {}; function MyComponent(props) { let x = props.foo; { let y = props.bar; const fn = useCallback(() => { // nothing }, []); } } ` }] }] }, { code: normalizeIndent` import MutableStore from 'store'; let z = {}; function MyComponent(props) { let x = props.foo; { let y = props.bar; const fn = useCallback(() => { // nothing }, [MutableStore?.hello?.world, props.foo, x, y, z, global?.stuff]); } } `, errors: [{ message: 'React Hook useCallback has unnecessary dependencies: ' + "'MutableStore.hello.world', 'global.stuff', 'props.foo', 'x', 'y', and 'z'. " + 'Either exclude them or remove the dependency array. ' + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + "because mutating them doesn't re-render the component.", suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` import MutableStore from 'store'; let z = {}; function MyComponent(props) { let x = props.foo; { let y = props.bar; const fn = useCallback(() => { // nothing }, []); } } ` }] }] }, { // Every almost-static function is tainted by a dynamic value. code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { setTimeout(() => console.log(taint)); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'handleNext1'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [handleNext1]', output: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { setTimeout(() => console.log(taint)); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, [handleNext1]); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, []); } ` }] }, { message: "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [handleNext2]', output: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { setTimeout(() => console.log(taint)); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, [handleNext2]); useMemo(() => { return Store.subscribe(handleNext3); }, []); } ` }] }, { message: "React Hook useMemo has a missing dependency: 'handleNext3'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [handleNext3]', output: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { setTimeout(() => console.log(taint)); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, [handleNext3]); } ` }] }] }, { // Regression test code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; // Shouldn't affect anything function handleChange() {} function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { console.log(taint); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'handleNext1'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [handleNext1]', output: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; // Shouldn't affect anything function handleChange() {} function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { console.log(taint); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, [handleNext1]); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, []); } ` }] }, { message: "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [handleNext2]', output: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; // Shouldn't affect anything function handleChange() {} function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { console.log(taint); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, [handleNext2]); useMemo(() => { return Store.subscribe(handleNext3); }, []); } ` }] }, { message: "React Hook useMemo has a missing dependency: 'handleNext3'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [handleNext3]', output: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; // Shouldn't affect anything function handleChange() {} function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { console.log(taint); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, [handleNext3]); } ` }] }] }, { // Regression test code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; // Shouldn't affect anything const handleChange = () => {}; function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { console.log(taint); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'handleNext1'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [handleNext1]', output: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; // Shouldn't affect anything const handleChange = () => {}; function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { console.log(taint); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, [handleNext1]); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, []); } ` }] }, { message: "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [handleNext2]', output: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; // Shouldn't affect anything const handleChange = () => {}; function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { console.log(taint); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, [handleNext2]); useMemo(() => { return Store.subscribe(handleNext3); }, []); } ` }] }, { message: "React Hook useMemo has a missing dependency: 'handleNext3'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [handleNext3]', output: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); let taint = props.foo; // Shouldn't affect anything const handleChange = () => {}; function handleNext1(value) { let value2 = value * taint; setState(value2); console.log('hello'); } const handleNext2 = (value) => { setState(taint(value)); console.log('hello'); }; let handleNext3 = function(value) { console.log(taint); dispatch({ type: 'x', value }); }; useEffect(() => { return Store.subscribe(handleNext1); }, []); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, []); useMemo(() => { return Store.subscribe(handleNext3); }, [handleNext3]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); function handleNext(value) { setState(value); } useEffect(() => { return Store.subscribe(handleNext); }, [handleNext]); } `, errors: [{ message: `The 'handleNext' function makes the dependencies of ` + `useEffect Hook (at line 11) change on every render. ` + `Move it inside the useEffect callback. Alternatively, ` + `wrap the definition of 'handleNext' in its own useCallback() Hook.`, // Not gonna fix a function definition // because it's not always safe due to hoisting. suggestions: undefined }] }, { // Even if the function only references static values, // once you specify it in deps, it will invalidate them. code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); const handleNext = (value) => { setState(value); }; useEffect(() => { return Store.subscribe(handleNext); }, [handleNext]); } `, errors: [{ message: `The 'handleNext' function makes the dependencies of ` + `useEffect Hook (at line 11) change on every render. ` + `Move it inside the useEffect callback. Alternatively, ` + `wrap the definition of 'handleNext' in its own useCallback() Hook.`, // We don't fix moving (too invasive). But that's the suggested fix // when only effect uses this function. Otherwise, we'd useCallback. suggestions: undefined }] }, { // Even if the function only references static values, // once you specify it in deps, it will invalidate them. // However, we can't suggest moving handleNext into the // effect because it is *also* used outside of it. // So our suggestion is useCallback(). code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); const handleNext = (value) => { setState(value); }; useEffect(() => { return Store.subscribe(handleNext); }, [handleNext]); return
; } `, errors: [{ message: `The 'handleNext' function makes the dependencies of ` + `useEffect Hook (at line 11) change on every render. ` + `To fix this, wrap the definition of 'handleNext' in its own useCallback() Hook.`, // We fix this one with useCallback since it's // the easy fix and you can't just move it into effect. suggestions: [{ desc: "Wrap the definition of 'handleNext' in its own useCallback() Hook.", output: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); const handleNext = useCallback((value) => { setState(value); }); useEffect(() => { return Store.subscribe(handleNext); }, [handleNext]); return
; } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { function handleNext1() { console.log('hello'); } const handleNext2 = () => { console.log('hello'); }; let handleNext3 = function() { console.log('hello'); }; useEffect(() => { return Store.subscribe(handleNext1); }, [handleNext1]); useLayoutEffect(() => { return Store.subscribe(handleNext2); }, [handleNext2]); useMemo(() => { return Store.subscribe(handleNext3); }, [handleNext3]); } `, errors: [{ message: "The 'handleNext1' function makes the dependencies of useEffect Hook " + '(at line 14) change on every render. Move it inside the useEffect callback. ' + "Alternatively, wrap the definition of 'handleNext1' in its own useCallback() Hook.", suggestions: undefined }, { message: "The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " + '(at line 17) change on every render. Move it inside the useLayoutEffect callback. ' + "Alternatively, wrap the definition of 'handleNext2' in its own useCallback() Hook.", suggestions: undefined }, { message: "The 'handleNext3' function makes the dependencies of useMemo Hook " + '(at line 20) change on every render. Move it inside the useMemo callback. ' + "Alternatively, wrap the definition of 'handleNext3' in its own useCallback() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function MyComponent(props) { function handleNext1() { console.log('hello'); } const handleNext2 = () => { console.log('hello'); }; let handleNext3 = function() { console.log('hello'); }; useEffect(() => { handleNext1(); return Store.subscribe(() => handleNext1()); }, [handleNext1]); useLayoutEffect(() => { handleNext2(); return Store.subscribe(() => handleNext2()); }, [handleNext2]); useMemo(() => { handleNext3(); return Store.subscribe(() => handleNext3()); }, [handleNext3]); } `, // Suggestions don't wrap into useCallback here // because they are only referenced by effect itself. errors: [{ message: "The 'handleNext1' function makes the dependencies of useEffect Hook " + '(at line 15) change on every render. Move it inside the useEffect callback. ' + "Alternatively, wrap the definition of 'handleNext1' in its own useCallback() Hook.", suggestions: undefined }, { message: "The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " + '(at line 19) change on every render. Move it inside the useLayoutEffect callback. ' + "Alternatively, wrap the definition of 'handleNext2' in its own useCallback() Hook.", suggestions: undefined }, { message: "The 'handleNext3' function makes the dependencies of useMemo Hook " + '(at line 23) change on every render. Move it inside the useMemo callback. ' + "Alternatively, wrap the definition of 'handleNext3' in its own useCallback() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function MyComponent(props) { function handleNext1() { console.log('hello'); } const handleNext2 = () => { console.log('hello'); }; let handleNext3 = function() { console.log('hello'); }; useEffect(() => { handleNext1(); return Store.subscribe(() => handleNext1()); }, [handleNext1]); useLayoutEffect(() => { handleNext2(); return Store.subscribe(() => handleNext2()); }, [handleNext2]); useMemo(() => { handleNext3(); return Store.subscribe(() => handleNext3()); }, [handleNext3]); return (
{ handleNext1(); setTimeout(handleNext2); setTimeout(() => { handleNext3(); }); }} /> ); } `, errors: [{ message: "The 'handleNext1' function makes the dependencies of useEffect Hook " + '(at line 15) change on every render. To fix this, wrap the ' + "definition of 'handleNext1' in its own useCallback() Hook.", suggestions: undefined }, { message: "The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " + '(at line 19) change on every render. To fix this, wrap the ' + "definition of 'handleNext2' in its own useCallback() Hook.", // Suggestion wraps into useCallback where possible (variables only) // because they are only referenced outside the effect. suggestions: [{ desc: "Wrap the definition of 'handleNext2' in its own useCallback() Hook.", output: normalizeIndent` function MyComponent(props) { function handleNext1() { console.log('hello'); } const handleNext2 = useCallback(() => { console.log('hello'); }); let handleNext3 = function() { console.log('hello'); }; useEffect(() => { handleNext1(); return Store.subscribe(() => handleNext1()); }, [handleNext1]); useLayoutEffect(() => { handleNext2(); return Store.subscribe(() => handleNext2()); }, [handleNext2]); useMemo(() => { handleNext3(); return Store.subscribe(() => handleNext3()); }, [handleNext3]); return (
{ handleNext1(); setTimeout(handleNext2); setTimeout(() => { handleNext3(); }); }} /> ); } ` }] }, { message: "The 'handleNext3' function makes the dependencies of useMemo Hook " + '(at line 23) change on every render. To fix this, wrap the ' + "definition of 'handleNext3' in its own useCallback() Hook.", // Autofix wraps into useCallback where possible (variables only) // because they are only referenced outside the effect. suggestions: [{ desc: "Wrap the definition of 'handleNext3' in its own useCallback() Hook.", output: normalizeIndent` function MyComponent(props) { function handleNext1() { console.log('hello'); } const handleNext2 = () => { console.log('hello'); }; let handleNext3 = useCallback(function() { console.log('hello'); }); useEffect(() => { handleNext1(); return Store.subscribe(() => handleNext1()); }, [handleNext1]); useLayoutEffect(() => { handleNext2(); return Store.subscribe(() => handleNext2()); }, [handleNext2]); useMemo(() => { handleNext3(); return Store.subscribe(() => handleNext3()); }, [handleNext3]); return (
{ handleNext1(); setTimeout(handleNext2); setTimeout(() => { handleNext3(); }); }} /> ); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { const handleNext1 = () => { console.log('hello'); }; function handleNext2() { console.log('hello'); } useEffect(() => { return Store.subscribe(handleNext1); return Store.subscribe(handleNext2); }, [handleNext1, handleNext2]); useEffect(() => { return Store.subscribe(handleNext1); return Store.subscribe(handleNext2); }, [handleNext1, handleNext2]); } `, // Normally we'd suggest moving handleNext inside an // effect. But it's used by more than one. So we // suggest useCallback() and use it for the autofix // where possible (variable but not declaration). // TODO: we could coalesce messages for the same function if it affects multiple Hooks. errors: [{ message: "The 'handleNext1' function makes the dependencies of useEffect Hook " + '(at line 12) change on every render. To fix this, wrap the ' + "definition of 'handleNext1' in its own useCallback() Hook.", suggestions: [{ desc: "Wrap the definition of 'handleNext1' in its own useCallback() Hook.", output: normalizeIndent` function MyComponent(props) { const handleNext1 = useCallback(() => { console.log('hello'); }); function handleNext2() { console.log('hello'); } useEffect(() => { return Store.subscribe(handleNext1); return Store.subscribe(handleNext2); }, [handleNext1, handleNext2]); useEffect(() => { return Store.subscribe(handleNext1); return Store.subscribe(handleNext2); }, [handleNext1, handleNext2]); } ` }] }, { message: "The 'handleNext1' function makes the dependencies of useEffect Hook " + '(at line 16) change on every render. To fix this, wrap the ' + "definition of 'handleNext1' in its own useCallback() Hook.", suggestions: [{ desc: "Wrap the definition of 'handleNext1' in its own useCallback() Hook.", output: normalizeIndent` function MyComponent(props) { const handleNext1 = useCallback(() => { console.log('hello'); }); function handleNext2() { console.log('hello'); } useEffect(() => { return Store.subscribe(handleNext1); return Store.subscribe(handleNext2); }, [handleNext1, handleNext2]); useEffect(() => { return Store.subscribe(handleNext1); return Store.subscribe(handleNext2); }, [handleNext1, handleNext2]); } ` }] }, { message: "The 'handleNext2' function makes the dependencies of useEffect Hook " + '(at line 12) change on every render. To fix this, wrap the ' + "definition of 'handleNext2' in its own useCallback() Hook.", suggestions: undefined }, { message: "The 'handleNext2' function makes the dependencies of useEffect Hook " + '(at line 16) change on every render. To fix this, wrap the ' + "definition of 'handleNext2' in its own useCallback() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function MyComponent(props) { let handleNext = () => { console.log('hello'); }; if (props.foo) { handleNext = () => { console.log('hello'); }; } useEffect(() => { return Store.subscribe(handleNext); }, [handleNext]); } `, errors: [{ message: "The 'handleNext' function makes the dependencies of useEffect Hook " + '(at line 13) change on every render. To fix this, wrap the definition of ' + "'handleNext' in its own useCallback() Hook.", // Normally we'd suggest moving handleNext inside an // effect. But it's used more than once. // TODO: our autofix here isn't quite sufficient because // it only wraps the first definition. But seems ok. suggestions: [{ desc: "Wrap the definition of 'handleNext' in its own useCallback() Hook.", output: normalizeIndent` function MyComponent(props) { let handleNext = useCallback(() => { console.log('hello'); }); if (props.foo) { handleNext = () => { console.log('hello'); }; } useEffect(() => { return Store.subscribe(handleNext); }, [handleNext]); } ` }] }] }, { code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let taint = props.foo; function handleNext(value) { let value2 = value * taint; setState(value2); console.log('hello'); } useEffect(() => { return Store.subscribe(handleNext); }, [handleNext]); } `, errors: [{ message: `The 'handleNext' function makes the dependencies of ` + `useEffect Hook (at line 14) change on every render. ` + `Move it inside the useEffect callback. Alternatively, wrap the ` + `definition of 'handleNext' in its own useCallback() Hook.`, suggestions: undefined }] }, { code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'count'. " + 'Either include it or remove the dependency array. ' + `You can also do a functional update 'setCount(c => ...)' if you ` + `only need 'count' in the 'setCount' call.`, suggestions: [{ desc: 'Update the dependencies array to be: [count]', output: normalizeIndent` function Counter() { let [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, [count]); return

{count}

; } ` }] }] }, { code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); let [increment, setIncrement] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count + increment); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'count' and 'increment'. " + 'Either include them or remove the dependency array. ' + `You can also do a functional update 'setCount(c => ...)' if you ` + `only need 'count' in the 'setCount' call.`, suggestions: [{ desc: 'Update the dependencies array to be: [count, increment]', output: normalizeIndent` function Counter() { let [count, setCount] = useState(0); let [increment, setIncrement] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count + increment); }, 1000); return () => clearInterval(id); }, [count, increment]); return

{count}

; } ` }] }] }, { code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); let [increment, setIncrement] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count => count + increment); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'increment'. " + 'Either include it or remove the dependency array. ' + `You can also replace multiple useState variables with useReducer ` + `if 'setCount' needs the current value of 'increment'.`, suggestions: [{ desc: 'Update the dependencies array to be: [increment]', output: normalizeIndent` function Counter() { let [count, setCount] = useState(0); let [increment, setIncrement] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count => count + increment); }, 1000); return () => clearInterval(id); }, [increment]); return

{count}

; } ` }] }] }, { code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); let increment = useCustomHook(); useEffect(() => { let id = setInterval(() => { setCount(count => count + increment); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } `, // This intentionally doesn't show the reducer message // because we don't know if it's safe for it to close over a value. // We only show it for state variables (and possibly props). errors: [{ message: "React Hook useEffect has a missing dependency: 'increment'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [increment]', output: normalizeIndent` function Counter() { let [count, setCount] = useState(0); let increment = useCustomHook(); useEffect(() => { let id = setInterval(() => { setCount(count => count + increment); }, 1000); return () => clearInterval(id); }, [increment]); return

{count}

; } ` }] }] }, { code: normalizeIndent` function Counter({ step }) { let [count, setCount] = useState(0); function increment(x) { return x + step; } useEffect(() => { let id = setInterval(() => { setCount(count => increment(count)); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } `, // This intentionally doesn't show the reducer message // because we don't know if it's safe for it to close over a value. // We only show it for state variables (and possibly props). errors: [{ message: "React Hook useEffect has a missing dependency: 'increment'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [increment]', output: normalizeIndent` function Counter({ step }) { let [count, setCount] = useState(0); function increment(x) { return x + step; } useEffect(() => { let id = setInterval(() => { setCount(count => increment(count)); }, 1000); return () => clearInterval(id); }, [increment]); return

{count}

; } ` }] }] }, { code: normalizeIndent` function Counter({ step }) { let [count, setCount] = useState(0); function increment(x) { return x + step; } useEffect(() => { let id = setInterval(() => { setCount(count => increment(count)); }, 1000); return () => clearInterval(id); }, [increment]); return

{count}

; } `, errors: [{ message: `The 'increment' function makes the dependencies of useEffect Hook ` + `(at line 14) change on every render. Move it inside the useEffect callback. ` + `Alternatively, wrap the definition of \'increment\' in its own ` + `useCallback() Hook.`, suggestions: undefined }] }, { code: normalizeIndent` function Counter({ increment }) { let [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count => count + increment); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'increment'. " + 'Either include it or remove the dependency array. ' + `If 'setCount' needs the current value of 'increment', ` + `you can also switch to useReducer instead of useState and read 'increment' in the reducer.`, suggestions: [{ desc: 'Update the dependencies array to be: [increment]', output: normalizeIndent` function Counter({ increment }) { let [count, setCount] = useState(0); useEffect(() => { let id = setInterval(() => { setCount(count => count + increment); }, 1000); return () => clearInterval(id); }, [increment]); return

{count}

; } ` }] }] }, { code: normalizeIndent` function Counter() { const [count, setCount] = useState(0); function tick() { setCount(count + 1); } useEffect(() => { let id = setInterval(() => { tick(); }, 1000); return () => clearInterval(id); }, []); return

{count}

; } `, // TODO: ideally this should suggest useState updater form // since this code doesn't actually work. The autofix could // at least avoid suggesting 'tick' since it's obviously // always different, and thus useless. errors: [{ message: "React Hook useEffect has a missing dependency: 'tick'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [tick]', output: normalizeIndent` function Counter() { const [count, setCount] = useState(0); function tick() { setCount(count + 1); } useEffect(() => { let id = setInterval(() => { tick(); }, 1000); return () => clearInterval(id); }, [tick]); return

{count}

; } ` }] }] }, { // Regression test for a crash code: normalizeIndent` function Podcasts() { useEffect(() => { alert(podcasts); }, []); let [podcasts, setPodcasts] = useState(null); } `, errors: [{ message: `React Hook useEffect has a missing dependency: 'podcasts'. ` + `Either include it or remove the dependency array.`, // Note: this autofix is shady because // the variable is used before declaration. // TODO: Maybe we can catch those fixes and not autofix. suggestions: [{ desc: 'Update the dependencies array to be: [podcasts]', output: normalizeIndent` function Podcasts() { useEffect(() => { alert(podcasts); }, [podcasts]); let [podcasts, setPodcasts] = useState(null); } ` }] }] }, { code: normalizeIndent` function Podcasts({ fetchPodcasts, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { fetchPodcasts(id).then(setPodcasts); }, [id]); } `, errors: [{ message: `React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` + `Either include it or remove the dependency array. ` + `If 'fetchPodcasts' changes too often, ` + `find the parent component that defines it and wrap that definition in useCallback.`, suggestions: [{ desc: 'Update the dependencies array to be: [fetchPodcasts, id]', output: normalizeIndent` function Podcasts({ fetchPodcasts, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { fetchPodcasts(id).then(setPodcasts); }, [fetchPodcasts, id]); } ` }] }] }, { code: normalizeIndent` function Podcasts({ api: { fetchPodcasts }, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { fetchPodcasts(id).then(setPodcasts); }, [id]); } `, errors: [{ message: `React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` + `Either include it or remove the dependency array. ` + `If 'fetchPodcasts' changes too often, ` + `find the parent component that defines it and wrap that definition in useCallback.`, suggestions: [{ desc: 'Update the dependencies array to be: [fetchPodcasts, id]', output: normalizeIndent` function Podcasts({ api: { fetchPodcasts }, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { fetchPodcasts(id).then(setPodcasts); }, [fetchPodcasts, id]); } ` }] }] }, { code: normalizeIndent` function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { setTimeout(() => { console.log(id); fetchPodcasts(id).then(setPodcasts); fetchPodcasts2(id).then(setPodcasts); }); }, [id]); } `, errors: [{ message: `React Hook useEffect has missing dependencies: 'fetchPodcasts' and 'fetchPodcasts2'. ` + `Either include them or remove the dependency array. ` + `If 'fetchPodcasts' changes too often, ` + `find the parent component that defines it and wrap that definition in useCallback.`, suggestions: [{ desc: 'Update the dependencies array to be: [fetchPodcasts, fetchPodcasts2, id]', output: normalizeIndent` function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { setTimeout(() => { console.log(id); fetchPodcasts(id).then(setPodcasts); fetchPodcasts2(id).then(setPodcasts); }); }, [fetchPodcasts, fetchPodcasts2, id]); } ` }] }] }, { code: normalizeIndent` function Podcasts({ fetchPodcasts, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { console.log(fetchPodcasts); fetchPodcasts(id).then(setPodcasts); }, [id]); } `, errors: [{ message: `React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` + `Either include it or remove the dependency array. ` + `If 'fetchPodcasts' changes too often, ` + `find the parent component that defines it and wrap that definition in useCallback.`, suggestions: [{ desc: 'Update the dependencies array to be: [fetchPodcasts, id]', output: normalizeIndent` function Podcasts({ fetchPodcasts, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { console.log(fetchPodcasts); fetchPodcasts(id).then(setPodcasts); }, [fetchPodcasts, id]); } ` }] }] }, { code: normalizeIndent` function Podcasts({ fetchPodcasts, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { console.log(fetchPodcasts); fetchPodcasts?.(id).then(setPodcasts); }, [id]); } `, errors: [{ message: `React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` + `Either include it or remove the dependency array. ` + `If 'fetchPodcasts' changes too often, ` + `find the parent component that defines it and wrap that definition in useCallback.`, suggestions: [{ desc: 'Update the dependencies array to be: [fetchPodcasts, id]', output: normalizeIndent` function Podcasts({ fetchPodcasts, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { console.log(fetchPodcasts); fetchPodcasts?.(id).then(setPodcasts); }, [fetchPodcasts, id]); } ` }] }] }, { // The mistake here is that it was moved inside the effect // so it can't be referenced in the deps array. code: normalizeIndent` function Thing() { useEffect(() => { const fetchData = async () => {}; fetchData(); }, [fetchData]); } `, errors: [{ message: `React Hook useEffect has an unnecessary dependency: 'fetchData'. ` + `Either exclude it or remove the dependency array.`, suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` function Thing() { useEffect(() => { const fetchData = async () => {}; fetchData(); }, []); } ` }] }] }, { code: normalizeIndent` function Hello() { const [state, setState] = useState(0); useEffect(() => { setState({}); }); } `, errors: [{ message: `React Hook useEffect contains a call to 'setState'. ` + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + `To fix this, pass [] as a second argument to the useEffect Hook.`, suggestions: [{ desc: 'Add dependencies array: []', output: normalizeIndent` function Hello() { const [state, setState] = useState(0); useEffect(() => { setState({}); }, []); } ` }] }] }, { code: normalizeIndent` function Hello() { const [data, setData] = useState(0); useEffect(() => { fetchData.then(setData); }); } `, errors: [{ message: `React Hook useEffect contains a call to 'setData'. ` + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + `To fix this, pass [] as a second argument to the useEffect Hook.`, suggestions: [{ desc: 'Add dependencies array: []', output: normalizeIndent` function Hello() { const [data, setData] = useState(0); useEffect(() => { fetchData.then(setData); }, []); } ` }] }] }, { code: normalizeIndent` function Hello({ country }) { const [data, setData] = useState(0); useEffect(() => { fetchData(country).then(setData); }); } `, errors: [{ message: `React Hook useEffect contains a call to 'setData'. ` + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + `To fix this, pass [country] as a second argument to the useEffect Hook.`, suggestions: [{ desc: 'Add dependencies array: [country]', output: normalizeIndent` function Hello({ country }) { const [data, setData] = useState(0); useEffect(() => { fetchData(country).then(setData); }, [country]); } ` }] }] }, { code: normalizeIndent` function Hello({ prop1, prop2 }) { const [state, setState] = useState(0); useEffect(() => { if (prop1) { setState(prop2); } }); } `, errors: [{ message: `React Hook useEffect contains a call to 'setState'. ` + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + `To fix this, pass [prop1, prop2] as a second argument to the useEffect Hook.`, suggestions: [{ desc: 'Add dependencies array: [prop1, prop2]', output: normalizeIndent` function Hello({ prop1, prop2 }) { const [state, setState] = useState(0); useEffect(() => { if (prop1) { setState(prop2); } }, [prop1, prop2]); } ` }] }] }, { code: normalizeIndent` function Thing() { useEffect(async () => {}, []); } `, errors: [{ message: `Effect callbacks are synchronous to prevent race conditions. ` + `Put the async function inside:\n\n` + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching', suggestions: undefined }] }, { code: normalizeIndent` function Thing() { useEffect(async () => {}); } `, errors: [{ message: `Effect callbacks are synchronous to prevent race conditions. ` + `Put the async function inside:\n\n` + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching', suggestions: undefined }] }, { code: normalizeIndent` function Example() { const foo = useCallback(() => { foo(); }, [foo]); } `, errors: [{ message: "React Hook useCallback has an unnecessary dependency: 'foo'. " + 'Either exclude it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` function Example() { const foo = useCallback(() => { foo(); }, []); } ` }] }] }, { code: normalizeIndent` function Example({ prop }) { const foo = useCallback(() => { prop.hello(foo); }, [foo]); const bar = useCallback(() => { foo(); }, [foo]); } `, errors: [{ message: "React Hook useCallback has a missing dependency: 'prop'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [prop]', output: normalizeIndent` function Example({ prop }) { const foo = useCallback(() => { prop.hello(foo); }, [prop]); const bar = useCallback(() => { foo(); }, [foo]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; function myEffect() { console.log(local); } useEffect(myEffect, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; function myEffect() { console.log(local); } useEffect(myEffect, [local]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; const myEffect = () => { console.log(local); }; useEffect(myEffect, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; const myEffect = () => { console.log(local); }; useEffect(myEffect, [local]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; const myEffect = function() { console.log(local); }; useEffect(myEffect, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; const myEffect = function() { console.log(local); }; useEffect(myEffect, [local]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; const myEffect = () => { otherThing(); }; const otherThing = () => { console.log(local); }; useEffect(myEffect, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'otherThing'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [otherThing]', output: normalizeIndent` function MyComponent() { const local = {}; const myEffect = () => { otherThing(); }; const otherThing = () => { console.log(local); }; useEffect(myEffect, [otherThing]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; const myEffect = debounce(() => { console.log(local); }, delay); useEffect(myEffect, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'myEffect'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [myEffect]', output: normalizeIndent` function MyComponent() { const local = {}; const myEffect = debounce(() => { console.log(local); }, delay); useEffect(myEffect, [myEffect]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; const myEffect = debounce(() => { console.log(local); }, delay); useEffect(myEffect, [local]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'myEffect'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [myEffect]', output: normalizeIndent` function MyComponent() { const local = {}; const myEffect = debounce(() => { console.log(local); }, delay); useEffect(myEffect, [myEffect]); } ` }] }] }, { code: normalizeIndent` function MyComponent({myEffect}) { useEffect(myEffect, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'myEffect'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [myEffect]', output: normalizeIndent` function MyComponent({myEffect}) { useEffect(myEffect, [myEffect]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; useEffect(debounce(() => { console.log(local); }, delay), []); } `, errors: [{ message: 'React Hook useEffect received a function whose dependencies ' + 'are unknown. Pass an inline function instead.', suggestions: [] }] }, { code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, []); } `, // Dangerous autofix is enabled due to the option: output: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [local]); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [local]); } ` }] }], // Keep this until major IDEs and VS Code FB ESLint plugin support Suggestions API. options: [{ enableDangerousAutofixThisMayCauseInfiniteLoops: true }] }, { code: normalizeIndent` function MyComponent(props) { let foo = {} useEffect(() => { foo.bar.baz = 43; props.foo.bar.baz = 1; }, []); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'foo.bar' and 'props.foo.bar'. " + 'Either include them or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [foo.bar, props.foo.bar]', output: normalizeIndent` function MyComponent(props) { let foo = {} useEffect(() => { foo.bar.baz = 43; props.foo.bar.baz = 1; }, [foo.bar, props.foo.bar]); } ` }] }] }, { code: normalizeIndent` function Component() { const foo = {}; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' object makes the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = []; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' array makes the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = () => {}; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' function makes the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the definition of 'foo' in its own " + 'useCallback() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = function bar(){}; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' function makes the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the definition of 'foo' in its own " + 'useCallback() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = class {}; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' class makes the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = true ? {} : "fine"; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' conditional could make the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = bar || {}; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' logical expression could make the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = bar ?? {}; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' logical expression could make the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = bar && {}; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' logical expression could make the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = bar ? baz ? {} : null : null; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' conditional could make the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { let foo = {}; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' object makes the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { var foo = {}; useMemo(() => foo, [foo]); } `, errors: [{ message: "The 'foo' object makes the dependencies of useMemo Hook (at line 4) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = {}; useCallback(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' object makes the dependencies of useCallback Hook (at line 6) change on every render. " + "Move it inside the useCallback callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = {}; useEffect(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' object makes the dependencies of useEffect Hook (at line 6) change on every render. " + "Move it inside the useEffect callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = {}; useLayoutEffect(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' object makes the dependencies of useLayoutEffect Hook (at line 6) change on every render. " + "Move it inside the useLayoutEffect callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Component() { const foo = {}; useImperativeHandle( ref, () => { console.log(foo); }, [foo] ); } `, errors: [{ message: "The 'foo' object makes the dependencies of useImperativeHandle Hook (at line 9) change on every render. " + "Move it inside the useImperativeHandle callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Foo(section) { const foo = section.section_components?.edges ?? []; useEffect(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' logical expression could make the dependencies of useEffect Hook (at line 6) change on every render. " + "Move it inside the useEffect callback. Alternatively, wrap the initialization of 'foo' in its own " + 'useMemo() Hook.', suggestions: undefined }] }, { code: normalizeIndent` function Foo(section) { const foo = {}; console.log(foo); useMemo(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' object makes the dependencies of useMemo Hook (at line 7) change on every render. " + "To fix this, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function Foo() { const foo = <>Hi!; useMemo(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' JSX fragment makes the dependencies of useMemo Hook (at line 6) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function Foo() { const foo =
Hi!
; useMemo(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' JSX element makes the dependencies of useMemo Hook (at line 6) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function Foo() { const foo = bar = {}; useMemo(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' assignment expression makes the dependencies of useMemo Hook (at line 6) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function Foo() { const foo = new String('foo'); // Note 'foo' will be boxed, and thus an object and thus compared by reference. useMemo(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' object construction makes the dependencies of useMemo Hook (at line 6) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function Foo() { const foo = new Map([]); useMemo(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' object construction makes the dependencies of useMemo Hook (at line 6) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function Foo() { const foo = /reg/; useMemo(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' regular expression makes the dependencies of useMemo Hook (at line 6) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function Foo() { class Bar {}; useMemo(() => { console.log(new Bar()); }, [Bar]); } `, errors: [{ message: "The 'Bar' class makes the dependencies of useMemo Hook (at line 6) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'Bar' in its own useMemo() Hook.", suggestions: undefined }] }, { code: normalizeIndent` function Foo() { const foo = {}; useLayoutEffect(() => { console.log(foo); }, [foo]); useEffect(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' object makes the dependencies of useLayoutEffect Hook (at line 6) change on every render. " + "To fix this, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }, { message: "The 'foo' object makes the dependencies of useEffect Hook (at line 9) change on every render. " + "To fix this, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }] }] }; if (__EXPERIMENTAL__) { tests.valid = [...tests.valid, { code: normalizeIndent` function MyComponent({ theme }) { const onStuff = useEffectEvent(() => { showNotification(theme); }); useEffect(() => { onStuff(); }, []); } ` }]; tests.invalid = [...tests.invalid, { code: normalizeIndent` function MyComponent({ theme }) { const onStuff = useEffectEvent(() => { showNotification(theme); }); useEffect(() => { onStuff(); }, [onStuff]); } `, errors: [{ message: 'Functions returned from `useEffectEvent` 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 = useEffectEvent(() => { showNotification(theme); }); useEffect(() => { onStuff(); }, []); } ` }] }] }]; } // Tests that are only valid/invalid across parsers supporting Flow const testsFlow = { valid: [ // Ignore Generic Type Variables for arrow functions { code: normalizeIndent` function Example({ prop }) { const bar = useEffect((a: T): Hello => { prop(); }, [prop]); } ` }], invalid: [{ code: normalizeIndent` function Foo() { const foo = ({}: any); useMemo(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' object makes the dependencies of useMemo Hook (at line 6) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }] }] }; // Tests that are only valid/invalid across parsers supporting TypeScript const testsTypescript = { valid: [{ // `ref` is still constant, despite the cast. code: normalizeIndent` function MyComponent() { const ref = useRef() as React.MutableRefObject; useEffect(() => { console.log(ref.current); }, []); } ` }, { code: normalizeIndent` function MyComponent() { const [state, setState] = React.useState(0); useEffect(() => { const someNumber: typeof state = 2; setState(prevState => prevState + someNumber); }, []) } ` }, { code: normalizeIndent` function App() { const foo = {x: 1}; React.useEffect(() => { const bar = {x: 2}; const baz = bar as typeof foo; console.log(baz); }, []); } ` }, { code: normalizeIndent` function App(props) { React.useEffect(() => { console.log(props.test); }, [props.test] as const); } ` }, { code: normalizeIndent` function App(props) { React.useEffect(() => { console.log(props.test); }, [props.test] as any); } ` }, { code: normalizeIndent` function App(props) { React.useEffect((() => { console.log(props.test); }) as any, [props.test]); } ` }, { code: normalizeIndent` function useMyThing(): void { useEffect(() => { let foo: T; console.log(foo); }, []); } ` }], invalid: [{ // `local` is still non-constant, despite the cast. code: normalizeIndent` function MyComponent() { const local = {} as string; useEffect(() => { console.log(local); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'local'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [local]', output: normalizeIndent` function MyComponent() { const local = {} as string; useEffect(() => { console.log(local); }, [local]); } ` }] }] }, { code: normalizeIndent` function App() { const foo = {x: 1}; const bar = {x: 2}; useEffect(() => { const baz = bar as typeof foo; console.log(baz); }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'bar'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [bar]', output: normalizeIndent` function App() { const foo = {x: 1}; const bar = {x: 2}; useEffect(() => { const baz = bar as typeof foo; console.log(baz); }, [bar]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const pizza = {}; useEffect(() => ({ crust: pizza.crust, toppings: pizza?.toppings, }), []); } `, errors: [{ message: "React Hook useEffect has missing dependencies: 'pizza.crust' and 'pizza?.toppings'. " + 'Either include them or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [pizza.crust, pizza?.toppings]', output: normalizeIndent` function MyComponent() { const pizza = {}; useEffect(() => ({ crust: pizza.crust, toppings: pizza?.toppings, }), [pizza.crust, pizza?.toppings]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const pizza = {}; useEffect(() => ({ crust: pizza?.crust, density: pizza.crust.density, }), []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'pizza.crust'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [pizza.crust]', output: normalizeIndent` function MyComponent() { const pizza = {}; useEffect(() => ({ crust: pizza?.crust, density: pizza.crust.density, }), [pizza.crust]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const pizza = {}; useEffect(() => ({ crust: pizza.crust, density: pizza?.crust.density, }), []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'pizza.crust'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [pizza.crust]', output: normalizeIndent` function MyComponent() { const pizza = {}; useEffect(() => ({ crust: pizza.crust, density: pizza?.crust.density, }), [pizza.crust]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const pizza = {}; useEffect(() => ({ crust: pizza?.crust, density: pizza?.crust.density, }), []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'pizza?.crust'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [pizza?.crust]', output: normalizeIndent` function MyComponent() { const pizza = {}; useEffect(() => ({ crust: pizza?.crust, density: pizza?.crust.density, }), [pizza?.crust]); } ` }] }] }, // Regression test. { code: normalizeIndent` function Example(props) { useEffect(() => { let topHeight = 0; topHeight = props.upperViewHeight; }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'props.upperViewHeight'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props.upperViewHeight]', output: normalizeIndent` function Example(props) { useEffect(() => { let topHeight = 0; topHeight = props.upperViewHeight; }, [props.upperViewHeight]); } ` }] }] }, // Regression test. { code: normalizeIndent` function Example(props) { useEffect(() => { let topHeight = 0; topHeight = props?.upperViewHeight; }, []); } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'props?.upperViewHeight'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [props?.upperViewHeight]', output: normalizeIndent` function Example(props) { useEffect(() => { let topHeight = 0; topHeight = props?.upperViewHeight; }, [props?.upperViewHeight]); } ` }] }] }, { code: normalizeIndent` function MyComponent() { const [state, setState] = React.useState(0); useEffect(() => { const someNumber: typeof state = 2; setState(prevState => prevState + someNumber + state); }, []) } `, errors: [{ message: "React Hook useEffect has a missing dependency: 'state'. " + 'Either include it or remove the dependency array. ' + `You can also do a functional update 'setState(s => ...)' ` + `if you only need 'state' in the 'setState' call.`, suggestions: [{ desc: 'Update the dependencies array to be: [state]', output: normalizeIndent` function MyComponent() { const [state, setState] = React.useState(0); useEffect(() => { const someNumber: typeof state = 2; setState(prevState => prevState + someNumber + state); }, [state]) } ` }] }] }, { code: normalizeIndent` function MyComponent() { const [state, setState] = React.useState(0); useMemo(() => { const someNumber: typeof state = 2; console.log(someNumber); }, [state]) } `, errors: [{ message: "React Hook useMemo has an unnecessary dependency: 'state'. " + 'Either exclude it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: []', output: normalizeIndent` function MyComponent() { const [state, setState] = React.useState(0); useMemo(() => { const someNumber: typeof state = 2; console.log(someNumber); }, []) } ` }] }] }, { code: normalizeIndent` function Foo() { const foo = {} as any; useMemo(() => { console.log(foo); }, [foo]); } `, errors: [{ message: "The 'foo' object makes the dependencies of useMemo Hook (at line 6) change on every render. " + "Move it inside the useMemo callback. Alternatively, wrap the initialization of 'foo' in its own useMemo() Hook.", suggestions: undefined }] }] }; // Tests that are only valid/invalid for `@typescript-eslint/parser@4.x` const testsTypescriptEslintParserV4 = { valid: [], invalid: [ // TODO: Should also be invalid as part of the JS test suite i.e. be invalid with babel eslint parsers. // It doesn't use any explicit types but any JS is still valid TS. { code: normalizeIndent` function Foo({ Component }) { React.useEffect(() => { console.log(); }, []); }; `, errors: [{ message: "React Hook React.useEffect has a missing dependency: 'Component'. " + 'Either include it or remove the dependency array.', suggestions: [{ desc: 'Update the dependencies array to be: [Component]', output: normalizeIndent` function Foo({ Component }) { React.useEffect(() => { console.log(); }, [Component]); }; ` }] }] }] }; // For easier local testing if (!process.env.CI) { let only = []; let skipped = []; (t => { if (t.skip) { delete t.skip; t |> skipped.push(%); } if (t.only) { delete t.only; t |> only.push(%); } }) |> [...tests.valid, ...tests.invalid, ...testsFlow.valid, ...testsFlow.invalid, ...testsTypescript.valid, ...testsTypescript.invalid, ...testsTypescriptEslintParserV4.valid, ...testsTypescriptEslintParserV4.invalid].forEach(%); const predicate = t => { if (only.length > 0) { return (t |> only.indexOf(%)) !== -1; } if (skipped.length > 0) { return (t |> skipped.indexOf(%)) === -1; } return true; }; tests.valid = predicate |> tests.valid.filter(%); tests.invalid = predicate |> tests.invalid.filter(%); testsFlow.valid = predicate |> testsFlow.valid.filter(%); testsFlow.invalid = predicate |> testsFlow.invalid.filter(%); testsTypescript.valid = predicate |> testsTypescript.valid.filter(%); testsTypescript.invalid = predicate |> testsTypescript.invalid.filter(%); } 'rules-of-hooks/exhaustive-deps' |> describe(%, () => { const parserOptionsV7 = { ecmaFeatures: { jsx: true }, ecmaVersion: 6, sourceType: 'module' }; const languageOptionsV9 = { ecmaVersion: 6, sourceType: 'module', parserOptions: { ecmaFeatures: { jsx: true } } }; const testsBabelEslint = { valid: [...testsFlow.valid, ...tests.valid], invalid: [...testsFlow.invalid, ...tests.invalid] }; new ESLintTesterV7({ parser: 'babel-eslint' |> require.resolve(%), parserOptions: parserOptionsV7 }).run('eslint: v7, parser: babel-eslint', ReactHooksESLintRule, testsBabelEslint); new ESLintTesterV9({ languageOptions: { ...languageOptionsV9, parser: '@babel/eslint-parser' |> require(%) } }).run('eslint: v9, parser: @babel/eslint-parser', ReactHooksESLintRule, testsBabelEslint); const testsTypescriptEslintParser = { valid: [...testsTypescript.valid, ...tests.valid], invalid: [...testsTypescript.invalid, ...tests.invalid] }; new ESLintTesterV7({ parser: '@typescript-eslint/parser-v2' |> require.resolve(%), parserOptions: parserOptionsV7 }).run('eslint: v7, parser: @typescript-eslint/parser@2.x', ReactHooksESLintRule, testsTypescriptEslintParser); new ESLintTesterV9({ languageOptions: { ...languageOptionsV9, parser: '@typescript-eslint/parser-v2' |> require(%) } }).run('eslint: v9, parser: @typescript-eslint/parser@2.x', ReactHooksESLintRule, testsTypescriptEslintParser); new ESLintTesterV7({ parser: '@typescript-eslint/parser-v3' |> require.resolve(%), parserOptions: parserOptionsV7 }).run('eslint: v7, parser: @typescript-eslint/parser@3.x', ReactHooksESLintRule, testsTypescriptEslintParser); new ESLintTesterV9({ languageOptions: { ...languageOptionsV9, parser: '@typescript-eslint/parser-v3' |> require(%) } }).run('eslint: v9, parser: @typescript-eslint/parser@3.x', ReactHooksESLintRule, testsTypescriptEslintParser); new ESLintTesterV7({ parser: '@typescript-eslint/parser-v4' |> require.resolve(%), parserOptions: parserOptionsV7 }).run('eslint: v7, parser: @typescript-eslint/parser@4.x', ReactHooksESLintRule, { valid: [...testsTypescriptEslintParserV4.valid, ...testsTypescriptEslintParser.valid], invalid: [...testsTypescriptEslintParserV4.invalid, ...testsTypescriptEslintParser.invalid] }); new ESLintTesterV9({ languageOptions: { ...languageOptionsV9, parser: '@typescript-eslint/parser-v4' |> require(%) } }).run('eslint: v9, parser: @typescript-eslint/parser@4.x', ReactHooksESLintRule, { valid: [...testsTypescriptEslintParserV4.valid, ...testsTypescriptEslintParser.valid], invalid: [...testsTypescriptEslintParserV4.invalid, ...testsTypescriptEslintParser.invalid] }); new ESLintTesterV7({ parser: '@typescript-eslint/parser-v5' |> require.resolve(%), parserOptions: parserOptionsV7 }).run('eslint: v7, parser: @typescript-eslint/parser@^5.0.0-0', ReactHooksESLintRule, { valid: [...testsTypescriptEslintParserV4.valid, ...testsTypescriptEslintParser.valid], invalid: [...testsTypescriptEslintParserV4.invalid, ...testsTypescriptEslintParser.invalid] }); new ESLintTesterV9({ languageOptions: { ...languageOptionsV9, parser: '@typescript-eslint/parser-v5' |> require(%) } }).run('eslint: v9, parser: @typescript-eslint/parser@^5.0.0-0', ReactHooksESLintRule, { valid: [...testsTypescriptEslintParserV4.valid, ...testsTypescriptEslintParser.valid], invalid: [...testsTypescriptEslintParserV4.invalid, ...testsTypescriptEslintParser.invalid] }); });