diff --git a/packages/eslint-plugin-react-hooks/README.md b/packages/eslint-plugin-react-hooks/README.md index e3b7d40431ea..bc2f7db42603 100644 --- a/packages/eslint-plugin-react-hooks/README.md +++ b/packages/eslint-plugin-react-hooks/README.md @@ -42,7 +42,9 @@ If you want more fine-grained configuration, you can instead add a snippet like "rules": { // ... "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn" + "react-hooks/exhaustive-deps": ["warn", { + "avoidObjects": true + }] } } ``` @@ -58,7 +60,8 @@ This option accepts a regex to match the names of custom Hooks that have depende "rules": { // ... "react-hooks/exhaustive-deps": ["warn", { - "additionalHooks": "(useMyCustomHook|useMyOtherCustomHook)" + "additionalHooks": "(useMyCustomHook|useMyOtherCustomHook)", + "avoidObjects": true }] } } diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 5c3c64c173fa..a4ec115fb5f0 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -7473,6 +7473,57 @@ const tests = { }, ], }, + { + code: normalizeIndent` + function MyComponent(obj) { + useEffect(() => {}, [obj]); + useLayoutEffect(() => {}, [obj]); + useCallback(() => {}, [obj]); + useMemo(() => {}, [obj]); + } + `, + // const { value } = query; + // console.log(value); + errors: [ + { + message: + "React Hook useEffect has an object in its dependency array: 'obj'. " + + 'Non-primitive dependencies may cause the hook to execute unnecessarily. ' + + 'Consider destructuring the object outside the useEffect call or using ' + + 'property accessors to refer to primitive values within the dependency ' + + 'array.', + suggestions: undefined, + }, + { + message: + "React Hook useLayoutEffect has an object in its dependency array: 'obj'. " + + 'Non-primitive dependencies may cause the hook to execute unnecessarily. ' + + 'Consider destructuring the object outside the useLayoutEffect call or ' + + 'using property accessors to refer to primitive values within the ' + + 'dependency array.', + suggestions: undefined, + }, + { + message: + "React Hook useCallback has an object in its dependency array: 'obj'. " + + 'Non-primitive dependencies may cause the hook to execute unnecessarily. ' + + 'Consider destructuring the object outside the useCallback call or ' + + 'using property accessors to refer to primitive values within the ' + + 'dependency array.', + suggestions: undefined, + }, + { + message: + "React Hook useMemo has an object in its dependency array: 'obj'. " + + 'Non-primitive dependencies may cause the hook to execute unnecessarily. ' + + 'Consider destructuring the object outside the useMemo call or ' + + 'using property accessors to refer to primitive values within the ' + + 'dependency array.', + suggestions: undefined, + }, + ], + options: [{avoidObjects: true}], + }, { code: normalizeIndent` function Foo() { diff --git a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js index 26d9688ac17c..5759703a0680 100644 --- a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js +++ b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js @@ -29,6 +29,9 @@ export default { additionalHooks: { type: 'string', }, + avoidObjects: { + type: 'boolean', + }, enableDangerousAutofixThisMayCauseInfiniteLoops: { type: 'boolean', }, @@ -45,6 +48,9 @@ export default { ? new RegExp(context.options[0].additionalHooks) : undefined; + const avoidObjects = + context.options && context.options[0] && context.options[0].avoidObjects; + const enableDangerousAutofixThisMayCauseInfiniteLoops = (context.options && context.options[0] && @@ -53,6 +59,7 @@ export default { const options = { additionalHooks, + avoidObjects, enableDangerousAutofixThisMayCauseInfiniteLoops, }; @@ -622,6 +629,24 @@ export default { if (declaredDependencyNode === null) { return; } + // If we see an object then add a special warning if the avoidObjects option is true. + if ( + declaredDependencyNode.type === 'Identifier' && + options && + options.avoidObjects + ) { + reportProblem({ + node: declaredDependencyNode, + message: + `React Hook ${context.getSource(reactiveHook)} has an object ` + + `in its dependency array: '${declaredDependencyNode.name}'. ` + + 'Non-primitive dependencies may cause the hook to execute ' + + 'unnecessarily. Consider destructuring the object outside ' + + `the ${reactiveHookName} call or using property accessors ` + + 'to refer to primitive values within the dependency array.', + }); + return; + } // If we see a spread element then add a special warning. if (declaredDependencyNode.type === 'SpreadElement') { reportProblem({ diff --git a/packages/eslint-plugin-react-hooks/src/index.js b/packages/eslint-plugin-react-hooks/src/index.js index 88e1ae71a2a7..cbeae99351fc 100644 --- a/packages/eslint-plugin-react-hooks/src/index.js +++ b/packages/eslint-plugin-react-hooks/src/index.js @@ -15,7 +15,12 @@ export const configs = { plugins: ['react-hooks'], rules: { 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn', + 'react-hooks/exhaustive-deps': [ + 'warn', + { + avoidObjects: true, + }, + ], }, }, };