diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index b479ce48521ca..a15c05e6eaafb 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -17,7 +17,7 @@ const ReactHooksESLintRule = /** * 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) + * @param {TemplateStringsArray} strings array of code strings (only one expected) */ function normalizeIndent(strings) { const codeLines = strings[0].split('\n'); @@ -7362,6 +7362,70 @@ const tests = { }, ], }, + { + code: normalizeIndent` + function Component({ foo = [] }) { + useMemo(() => foo, [foo]); + } + `, + errors: [ + { + message: + "The 'foo' array makes the dependencies of useMemo Hook (at line 3) 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 useCustomHook(foo = []) { + useMemo(() => foo, [foo]); + } + `, + errors: [ + { + message: + "The 'foo' array makes the dependencies of useMemo Hook (at line 3) 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 useCustomHook(bar, foo = {}) { + useMemo(() => foo, [foo]); + } + `, + errors: [ + { + message: + "The 'foo' object makes the dependencies of useMemo Hook (at line 3) 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({ foo = {} }) { + useMemo(() => foo, [foo]); + } + `, + errors: [ + { + message: + "The 'foo' object makes the dependencies of useMemo Hook (at line 3) 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() { diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 05321ffb46f6e..0dc0290285bb8 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -1759,6 +1759,43 @@ function getConstructionExpressionType(node: Node): string | null { return null; } +/** + * Function parameters can contain assignment (to set + * the default value). + * This function scans the parameters, and returns + * the identifer's default value, if it has one. + */ +function findIdentifierAssignmentInArguments( + params: Array, + identifierName: string, +): Expression | null { + for (const param of params) { + // an object argument like `function ({ x = …, y = … }) {}` + if (param.type === 'ObjectPattern') { + for (const prop of param.properties) { + if ( + prop.type === 'Property' && + prop.key.type === 'Identifier' && + prop.key.name === identifierName && + prop.value.type === 'AssignmentPattern' + ) { + return prop.value.right; + } + } + } + + // a normal argument like `function (x = …, y = …) {}` + if ( + param.type === 'AssignmentPattern' && + param.left.type === 'Identifier' && + param.left.name === identifierName + ) { + return param.right; + } + } + return null; +} + // Finds variables declared as dependencies // that would invalidate on every render. function scanForConstructions({ @@ -1813,6 +1850,23 @@ function scanForConstructions({ if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') { return [ref, 'class']; } + + // function ({ x = [] }) {} + // or + // function (x = []) {} + if (node.type === 'Parameter') { + const value = findIdentifierAssignmentInArguments( + node.node.params, + key, + ); + if (value) { + const constantExpressionType = getConstructionExpressionType(value); + if (constantExpressionType) { + return [ref, constantExpressionType]; + } + } + } + return null; }) .filter(Boolean) as Array<[Scope.Variable, string]>;