react/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
Joseph Savona 7d29ecbeb2
[compiler] Aggregate error reporting, separate eslint rules (#34176)
NOTE: this is a merged version of @mofeiZ's original PR along with my
edits per offline discussion. The description is updated to reflect the
latest approach.

The key problem we're trying to solve with this PR is to allow
developers more control over the compiler's various validations. The
idea is to have a number of rules targeting a specific category of
issues, such as enforcing immutability of props/state/etc or disallowing
access to refs during render. We don't want to have to run the compiler
again for every single rule, though, so @mofeiZ added an LRU cache that
caches the full compilation output of N most recent files. The first
rule to run on a given file will cause it to get cached, and then
subsequent rules can pull from the cache, with each rule filtering down
to its specific category of errors.

For the categories, I went through and assigned a category roughly 1:1
to existing validations, and then used my judgement on some places that
felt distinct enough to warrant a separate error. Every error in the
compiler now has to supply both a severity (for legacy reasons) and a
category (for ESLint). Each category corresponds 1:1 to a ESLint rule
definition, so that the set of rules is automatically populated based on
the defined categories.

Categories include a flag for whether they should be in the recommended
set or not.

Note that as with the original version of this PR, only
eslint-plugin-react-compiler is changed. We still have to update the
main lint rule.

## Test Plan

* Created a sample project using ESLint v9 and verified that the plugin
can be configured correctly and detects errors
* Edited `fixtures/eslint-v9` and introduced errors, verified that the w
latest config changes in that fixture it correctly detects the errors
* In the sample project, confirmed that the LRU caching is correctly
caching compiler output, ie compiling files just once.

Co-authored-by: Mofei Zhang <feifei0@meta.com>
2025-08-21 14:53:34 -07:00

8675 lines
257 KiB
JavaScript

/**
* 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 = require('eslint-v7').RuleTester;
const ESLintTesterV9 = require('eslint-v9').RuleTester;
const ReactHooksESLintPlugin = require('eslint-plugin-react-hooks');
const ReactHooksESLintRule =
ReactHooksESLintPlugin.default.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 = strings[0].split('\n');
const leftPadding = codeLines[1].match(/\s+/)[0];
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
}
// ***************************************************
// 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'}],
},
{
// behaves like no deps
code: normalizeIndent`
function MyComponent(props) {
useSpecialEffect(() => {
console.log(props.foo);
}, null);
}
`,
options: [
{
additionalHooks: 'useSpecialEffect',
experimental_autoDependenciesHooks: ['useSpecialEffect'],
},
],
},
{
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 [state9, dispatch5] = useActionState();
const [state10, dispatch6] = React.useActionState();
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();
dispatch5();
dispatch6();
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, state9, state10,
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 <div />;
}
`,
},
{
// 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 <div />;
}
`,
},
{
// 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 <div ref={myRef} />;
}
`,
},
{
// 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 <div ref={myRef} />;
}
`,
},
{
// 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 <div ref={myRef} />;
}
`,
},
{
// 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 <div ref={myRef} />;
}
`,
},
{
// 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 <h1>{count}</h1>;
}
`,
},
{
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 <h1>{count}</h1>;
}
`,
},
{
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 <h1>{count}</h1>;
}
`,
},
{
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 <h1>{count}</h1>;
}
`,
},
{
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 <h1>{count}</h1>;
}
`,
},
{
// 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 <h1>{count}</h1>;
}
`,
},
{
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 <h1>{count}</h1>;
}
`,
},
{
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 <h1>{count}</h1>;
}
`,
},
{
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 <h1>{count}</h1>;
}
}
`,
},
{
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 (
<>
<input value={query} onChange={e => 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) {
useSpecialEffect(() => {
console.log(props.foo);
}, null);
}
`,
options: [{additionalHooks: 'useSpecialEffect'}],
errors: [
{
message:
"React Hook useSpecialEffect 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.",
},
{
message:
"React Hook useSpecialEffect 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) {
useSpecialEffect(() => {
console.log(props.foo);
}, [props.foo]);
}
`,
},
],
},
],
},
{
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]);
}
`,
},
],
},
],
},
{
// Affected code should use React.useActionState instead
code: normalizeIndent`
function ComponentUsingFormState(props) {
const [state7, dispatch3] = useFormState();
const [state8, dispatch4] = ReactDOM.useFormState();
useEffect(() => {
dispatch3();
dispatch4();
// dynamic
console.log(state7);
console.log(state8);
}, [state7, state8]);
}
`,
errors: [
{
message:
"React Hook useEffect has missing dependencies: 'dispatch3' and 'dispatch4'. " +
'Either include them or remove the dependency array.',
suggestions: [
{
desc: 'Update the dependencies array to be: [dispatch3, dispatch4, state7, state8]',
output: normalizeIndent`
function ComponentUsingFormState(props) {
const [state7, dispatch3] = useFormState();
const [state8, dispatch4] = ReactDOM.useFormState();
useEffect(() => {
dispatch3();
dispatch4();
// dynamic
console.log(state7);
console.log(state8);
}, [dispatch3, dispatch4, state7, state8]);
}
`,
},
],
},
],
},
{
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 <h1>{count}</h1>;
}
`,
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 <h1>{count}</h1>;
}
`,
},
],
},
],
},
{
// 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 <div ref={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 MyComponent() {
const myRef = useRef();
useEffect(() => {
const handleMove = () => {};
myRef?.current?.addEventListener('mousemove', handleMove);
return () => myRef?.current?.removeEventListener('mousemove', handleMove);
}, []);
return <div ref={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 MyComponent() {
const myRef = useRef();
useEffect(() => {
const handleMove = () => {};
myRef.current.addEventListener('mousemove', handleMove);
return () => myRef.current.removeEventListener('mousemove', handleMove);
});
return <div ref={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 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 <div ref={myRef} />;
}
`,
// 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 <div onClick={handleNext} />;
}
`,
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 <div onClick={handleNext} />;
}
`,
},
],
},
],
},
{
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 (
<div
onClick={() => {
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 (
<div
onClick={() => {
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 (
<div
onClick={() => {
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 <h1>{count}</h1>;
}
`,
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 <h1>{count}</h1>;
}
`,
},
],
},
],
},
{
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 <h1>{count}</h1>;
}
`,
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 <h1>{count}</h1>;
}
`,
},
],
},
],
},
{
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 <h1>{count}</h1>;
}
`,
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 <h1>{count}</h1>;
}
`,
},
],
},
],
},
{
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 <h1>{count}</h1>;
}
`,
// 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 <h1>{count}</h1>;
}
`,
},
],
},
],
},
{
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 <h1>{count}</h1>;
}
`,
// 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 <h1>{count}</h1>;
}
`,
},
],
},
],
},
{
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 <h1>{count}</h1>;
}
`,
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 <h1>{count}</h1>;
}
`,
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 <h1>{count}</h1>;
}
`,
},
],
},
],
},
{
code: normalizeIndent`
function Counter() {
const [count, setCount] = useState(0);
function tick() {
setCount(count + 1);
}
useEffect(() => {
let id = setInterval(() => {
tick();
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
`,
// 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 <h1>{count}</h1>;
}
`,
},
],
},
],
},
{
// 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 received a function whose dependencies are unknown. Pass an inline function instead.',
},
],
},
{
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 = <div>Hi!</div>;
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,
},
],
},
{
code: normalizeIndent`
function useCustomCallback(callback, deps) {
return useCallback(callback, deps)
}
`,
errors: [
{
message:
'React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.',
},
],
},
],
};
if (__EXPERIMENTAL__) {
tests.valid = [
...tests.valid,
{
code: normalizeIndent`
function MyComponent({ theme }) {
const onStuff = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onStuff();
}, []);
React.useEffect(() => {
onStuff();
}, []);
}
`,
},
];
tests.invalid = [
...tests.invalid,
{
code: normalizeIndent`
function MyComponent({ theme }) {
const onStuff = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onStuff();
}, [onStuff]);
React.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();
}, []);
React.useEffect(() => {
onStuff();
}, [onStuff]);
}
`,
},
],
},
{
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();
}, [onStuff]);
React.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(<T>(a: T): Hello => {
prop();
}, [prop]);
}
`,
},
],
invalid: [
{
code: normalizeIndent`
hook useExample(a) {
useEffect(() => {
console.log(a);
}, []);
}
`,
errors: [
{
message:
"React Hook useEffect has a missing dependency: 'a'. " +
'Either include it or remove the dependency array.',
suggestions: [
{
desc: 'Update the dependencies array to be: [a]',
output: normalizeIndent`
hook useExample(a) {
useEffect(() => {
console.log(a);
}, [a]);
}
`,
},
],
},
],
},
{
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<HTMLDivElement>;
useEffect(() => {
console.log(ref.current);
}, []);
}
`,
},
{
code: normalizeIndent`
function MyComponent() {
const [state, setState] = React.useState<number>(0);
useEffect(() => {
const someNumber: typeof state = 2;
setState(prevState => prevState + someNumber);
}, [])
}
`,
},
{
code: normalizeIndent`
function MyComponent() {
const [state, setState] = React.useState<number>(0);
useSpecialEffect(() => {
const someNumber: typeof state = 2;
setState(prevState => prevState + someNumber);
})
}
`,
options: [
{
additionalHooks: 'useSpecialEffect',
experimental_autoDependenciesHooks: ['useSpecialEffect'],
},
],
},
{
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<T>(): 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<number>(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<number>(0);
useEffect(() => {
const someNumber: typeof state = 2;
setState(prevState => prevState + someNumber + state);
}, [state])
}
`,
},
],
},
],
},
{
code: normalizeIndent`
function MyComponent() {
const [state, setState] = React.useState<number>(0);
useSpecialEffect(() => {
const someNumber: typeof state = 2;
setState(prevState => prevState + someNumber + state);
}, [])
}
`,
options: [
{
additionalHooks: 'useSpecialEffect',
experimental_autoDependenciesHooks: ['useSpecialEffect'],
},
],
errors: [
{
message:
"React Hook useSpecialEffect 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<number>(0);
useSpecialEffect(() => {
const someNumber: typeof state = 2;
setState(prevState => prevState + someNumber + state);
}, [state])
}
`,
},
],
},
],
},
{
code: normalizeIndent`
function MyComponent() {
const [state, setState] = React.useState<number>(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<number>(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,
},
],
},
{
code: normalizeIndent`
function useCustomCallback(callback, deps) {
return useCallback(callback as any, deps)
}
`,
errors: [
{
message:
'React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.',
},
],
},
{
code: normalizeIndent`
function MyComponent(props) {
useEffect(() => {
console.log(props.foo);
});
}
`,
options: [{requireExplicitEffectDeps: true}],
errors: [
{
message:
'React Hook useEffect always requires dependencies. Please add a dependency array or an explicit `undefined`',
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(<Component />);
}, []);
};
`,
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 />);
}, [Component]);
};
`,
},
],
},
],
},
],
};
// For easier local testing
if (!process.env.CI) {
let only = [];
let skipped = [];
[
...tests.valid,
...tests.invalid,
...testsFlow.valid,
...testsFlow.invalid,
...testsTypescript.valid,
...testsTypescript.invalid,
...testsTypescriptEslintParserV4.valid,
...testsTypescriptEslintParserV4.invalid,
].forEach(t => {
if (t.skip) {
delete t.skip;
skipped.push(t);
}
if (t.only) {
delete t.only;
only.push(t);
}
});
const predicate = t => {
if (only.length > 0) {
return only.indexOf(t) !== -1;
}
if (skipped.length > 0) {
return skipped.indexOf(t) === -1;
}
return true;
};
tests.valid = tests.valid.filter(predicate);
tests.invalid = tests.invalid.filter(predicate);
testsFlow.valid = testsFlow.valid.filter(predicate);
testsFlow.invalid = testsFlow.invalid.filter(predicate);
testsTypescript.valid = testsTypescript.valid.filter(predicate);
testsTypescript.invalid = testsTypescript.invalid.filter(predicate);
testsTypescriptEslintParserV4.valid =
testsTypescriptEslintParserV4.valid.filter(predicate);
testsTypescriptEslintParserV4.invalid =
testsTypescriptEslintParserV4.invalid.filter(predicate);
}
describe('rules-of-hooks/exhaustive-deps', () => {
const parserOptionsV7 = {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 6,
sourceType: 'module',
};
const languageOptionsV9 = {
ecmaVersion: 6,
sourceType: 'module',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
};
const testsBabelEslint = tests;
const testsHermesParser = {
valid: [...testsFlow.valid, ...tests.valid],
invalid: [...testsFlow.invalid, ...tests.invalid],
};
new ESLintTesterV7({
parser: require.resolve('babel-eslint'),
parserOptions: parserOptionsV7,
}).run(
'eslint: v7, parser: babel-eslint',
ReactHooksESLintRule,
testsBabelEslint
);
new ESLintTesterV9({
languageOptions: {
...languageOptionsV9,
parser: require('@babel/eslint-parser'),
},
}).run(
'eslint: v9, parser: @babel/eslint-parser',
ReactHooksESLintRule,
testsBabelEslint
);
new ESLintTesterV7({
parser: require.resolve('hermes-eslint'),
parserOptions: {
sourceType: 'module',
enableExperimentalComponentSyntax: true,
},
}).run(
'eslint: v7, parser: hermes-eslint',
ReactHooksESLintRule,
testsHermesParser
);
new ESLintTesterV9({
languageOptions: {
...languageOptionsV9,
parser: require('hermes-eslint'),
parserOptions: {
sourceType: 'module',
enableExperimentalComponentSyntax: true,
},
},
}).run(
'eslint: v9, parser: hermes-eslint',
ReactHooksESLintRule,
testsHermesParser
);
const testsTypescriptEslintParser = {
valid: [...testsTypescript.valid, ...tests.valid],
invalid: [...testsTypescript.invalid, ...tests.invalid],
};
new ESLintTesterV7({
parser: require.resolve('@typescript-eslint/parser-v2'),
parserOptions: parserOptionsV7,
}).run(
'eslint: v7, parser: @typescript-eslint/parser@2.x',
ReactHooksESLintRule,
testsTypescriptEslintParser
);
new ESLintTesterV9({
languageOptions: {
...languageOptionsV9,
parser: require('@typescript-eslint/parser-v2'),
},
}).run(
'eslint: v9, parser: @typescript-eslint/parser@2.x',
ReactHooksESLintRule,
testsTypescriptEslintParser
);
new ESLintTesterV7({
parser: require.resolve('@typescript-eslint/parser-v3'),
parserOptions: parserOptionsV7,
}).run(
'eslint: v7, parser: @typescript-eslint/parser@3.x',
ReactHooksESLintRule,
testsTypescriptEslintParser
);
new ESLintTesterV9({
languageOptions: {
...languageOptionsV9,
parser: require('@typescript-eslint/parser-v3'),
},
}).run(
'eslint: v9, parser: @typescript-eslint/parser@3.x',
ReactHooksESLintRule,
testsTypescriptEslintParser
);
new ESLintTesterV7({
parser: require.resolve('@typescript-eslint/parser-v4'),
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: require('@typescript-eslint/parser-v4'),
},
}).run(
'eslint: v9, parser: @typescript-eslint/parser@4.x',
ReactHooksESLintRule,
{
valid: [
...testsTypescriptEslintParserV4.valid,
...testsTypescriptEslintParser.valid,
],
invalid: [
...testsTypescriptEslintParserV4.invalid,
...testsTypescriptEslintParser.invalid,
],
}
);
new ESLintTesterV7({
parser: require.resolve('@typescript-eslint/parser-v5'),
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: require('@typescript-eslint/parser-v5'),
},
}).run(
'eslint: v9, parser: @typescript-eslint/parser@^5.0.0',
ReactHooksESLintRule,
{
valid: [
...testsTypescriptEslintParserV4.valid,
...testsTypescriptEslintParser.valid,
],
invalid: [
...testsTypescriptEslintParserV4.invalid,
...testsTypescriptEslintParser.invalid,
],
}
);
});