Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1564,8 +1564,111 @@ const tests = {
}
`,
},
{
code: normalizeIndent`
function MyComponent() {
const [count, setCount] = useState(0);

const logCount = () => {
console.log(count);
};

useEffect(() => {
logCount();
}, [logCount]);
}
`,
options: [{reactCompiler: true}],
},
{
code: normalizeIndent`
function MyComponent() {
const [count, setCount] = useState(0);

const options = { count };

useEffect(() => {
console.log(options);
}, [options]);
}
`,
options: [{reactCompiler: true}],
},
{
code: normalizeIndent`
function MyComponent() {
const [count, setCount] = useState(0);

const logCount = () => {
console.log(count);
};

useEffect(() => {
logCount();
}, [logCount]);
}
`,
settings: {reactCompiler: true},
},
],
invalid: [
{
code: normalizeIndent`
function MyComponent() {
const [count, setCount] = useState(0);

const logCount = () => {
console.log(count);
};

useEffect(() => {
logCount();
}, [logCount]);
}
`,
errors: [
{
message:
"The 'logCount' 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 'logCount' in its own useCallback() Hook.",
suggestions: undefined,
},
],
},
{
code: normalizeIndent`
function MyComponent() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(count);
}, []);
}
`,
options: [{reactCompiler: true}],
errors: [
{
message:
"React Hook useEffect has a missing dependency: 'count'. " +
'Either include it or remove the dependency array.',
suggestions: [
{
desc: 'Update the dependencies array to be: [count]',
output: normalizeIndent`
function MyComponent() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(count);
}, [count]);
}
`,
},
],
},
],
},
{
code: normalizeIndent`
function MyComponent(props) {
Expand Down
142 changes: 77 additions & 65 deletions packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ const rule = {
requireExplicitEffectDeps: {
type: 'boolean',
},
reactCompiler: {
type: 'boolean',
},
},
},
],
Expand Down Expand Up @@ -101,11 +104,17 @@ const rule = {
const requireExplicitEffectDeps: boolean =
(rawOptions && rawOptions.requireExplicitEffectDeps) || false;

const reactCompiler: boolean =
(rawOptions && rawOptions.reactCompiler) ||
(settings && settings.reactCompiler) ||
false;

const options = {
additionalHooks,
experimental_autoDependenciesHooks,
enableDangerousAutofixThisMayCauseInfiniteLoops,
requireExplicitEffectDeps,
reactCompiler,
};

function reportProblem(problem: Rule.ReportDescriptor) {
Expand Down Expand Up @@ -907,78 +916,81 @@ const rule = {
if (problemCount === 0) {
// If nothing else to report, check if some dependencies would
// invalidate on every render.
const constructions = scanForConstructions({
declaredDependencies,
declaredDependenciesNode,
componentScope,
scope,
});
constructions.forEach(
({construction, isUsedOutsideOfHook, depType}) => {
const wrapperHook =
depType === 'function' ? 'useCallback' : 'useMemo';
// Skip this check if React Compiler is enabled since it auto-memoizes
if (!options.reactCompiler) {
const constructions = scanForConstructions({
declaredDependencies,
declaredDependenciesNode,
componentScope,
scope,
});
constructions.forEach(
({construction, isUsedOutsideOfHook, depType}) => {
const wrapperHook =
depType === 'function' ? 'useCallback' : 'useMemo';

const constructionType =
depType === 'function' ? 'definition' : 'initialization';
const constructionType =
depType === 'function' ? 'definition' : 'initialization';

const defaultAdvice = `wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`;
const defaultAdvice = `wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`;

const advice = isUsedOutsideOfHook
? `To fix this, ${defaultAdvice}`
: `Move it inside the ${reactiveHookName} callback. Alternatively, ${defaultAdvice}`;
const advice = isUsedOutsideOfHook
? `To fix this, ${defaultAdvice}`
: `Move it inside the ${reactiveHookName} callback. Alternatively, ${defaultAdvice}`;

const causation =
depType === 'conditional' || depType === 'logical expression'
? 'could make'
: 'makes';
const causation =
depType === 'conditional' || depType === 'logical expression'
? 'could make'
: 'makes';

const message =
`The '${construction.name.name}' ${depType} ${causation} the dependencies of ` +
`${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc?.start.line}) ` +
`change on every render. ${advice}`;
const message =
`The '${construction.name.name}' ${depType} ${causation} the dependencies of ` +
`${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc?.start.line}) ` +
`change on every render. ${advice}`;

let suggest: Rule.ReportDescriptor['suggest'];
// Only handle the simple case of variable assignments.
// Wrapping function declarations can mess up hoisting.
if (
isUsedOutsideOfHook &&
construction.type === 'Variable' &&
// Objects may be mutated after construction, which would make this
// fix unsafe. Functions _probably_ won't be mutated, so we'll
// allow this fix for them.
depType === 'function'
) {
suggest = [
{
desc: `Wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`,
fix(fixer) {
const [before, after] =
wrapperHook === 'useMemo'
? [`useMemo(() => { return `, '; })']
: ['useCallback(', ')'];
return [
// TODO: also add an import?
fixer.insertTextBefore(construction.node.init!, before),
// TODO: ideally we'd gather deps here but it would require
// restructuring the rule code. This will cause a new lint
// error to appear immediately for useCallback. Note we're
// not adding [] because would that changes semantics.
fixer.insertTextAfter(construction.node.init!, after),
];
let suggest: Rule.ReportDescriptor['suggest'];
// Only handle the simple case of variable assignments.
// Wrapping function declarations can mess up hoisting.
if (
isUsedOutsideOfHook &&
construction.type === 'Variable' &&
// Objects may be mutated after construction, which would make this
// fix unsafe. Functions _probably_ won't be mutated, so we'll
// allow this fix for them.
depType === 'function'
) {
suggest = [
{
desc: `Wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`,
fix(fixer) {
const [before, after] =
wrapperHook === 'useMemo'
? [`useMemo(() => { return `, '; })']
: ['useCallback(', ')'];
return [
// TODO: also add an import?
fixer.insertTextBefore(construction.node.init!, before),
// TODO: ideally we'd gather deps here but it would require
// restructuring the rule code. This will cause a new lint
// error to appear immediately for useCallback. Note we're
// not adding [] because would that changes semantics.
fixer.insertTextAfter(construction.node.init!, after),
];
},
},
},
];
}
// TODO: What if the function needs to change on every render anyway?
// Should we suggest removing effect deps as an appropriate fix too?
reportProblem({
// TODO: Why not report this at the dependency site?
node: construction.node,
message,
suggest,
});
},
);
];
}
// TODO: What if the function needs to change on every render anyway?
// Should we suggest removing effect deps as an appropriate fix too?
reportProblem({
// TODO: Why not report this at the dependency site?
node: construction.node,
message,
suggest,
});
},
);
}
return;
}

Expand Down