From 7fd0a0fed02c01a15b7c16fef0b575afb39fffcd Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Tue, 5 May 2026 16:31:18 +0530 Subject: [PATCH] Preserve optional chains in exhaustive-deps suggestions --- .../ESLintRuleExhaustiveDeps-test.js | 46 ++++++++++++++++--- .../src/rules/ExhaustiveDeps.ts | 12 ++--- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 29e956d314a2..b191708d9dc9 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -1755,6 +1755,40 @@ const tests = { }, ], }, + { + code: normalizeIndent` + function MyComponent({foo, one}) { + useEffect(() => { + console.log(one); + if (foo?.bar) { + console.log(foo.bar); + } + }, [one]); + } + `, + errors: [ + { + message: + "React Hook useEffect has a missing dependency: 'foo?.bar'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [foo?.bar, one]', + output: normalizeIndent` + function MyComponent({foo, one}) { + useEffect(() => { + console.log(one); + if (foo?.bar) { + console.log(foo.bar); + } + }, [foo?.bar, one]); + } + `, + }, + ], + }, + ], + }, { code: normalizeIndent` function MyComponent() { @@ -8191,11 +8225,11 @@ const testsTypescript = { errors: [ { message: - "React Hook useEffect has a missing dependency: 'pizza.crust'. " + + "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]', + desc: 'Update the dependencies array to be: [pizza?.crust]', output: normalizeIndent` function MyComponent() { const pizza = {}; @@ -8203,7 +8237,7 @@ const testsTypescript = { useEffect(() => ({ crust: pizza?.crust, density: pizza.crust.density, - }), [pizza.crust]); + }), [pizza?.crust]); } `, }, @@ -8225,11 +8259,11 @@ const testsTypescript = { errors: [ { message: - "React Hook useEffect has a missing dependency: 'pizza.crust'. " + + "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]', + desc: 'Update the dependencies array to be: [pizza?.crust]', output: normalizeIndent` function MyComponent() { const pizza = {}; @@ -8237,7 +8271,7 @@ const testsTypescript = { useEffect(() => ({ crust: pizza.crust, density: pizza?.crust.density, - }), [pizza.crust]); + }), [pizza?.crust]); } `, }, diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 6b790680608d..d1d33ce72fa1 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -1905,13 +1905,11 @@ function markNode( ): void { if (optionalChains) { if ('optional' in node && node.optional) { - // We only want to consider it optional if *all* usages were optional. - if (!optionalChains.has(result)) { - // Mark as (maybe) optional. If there's a required usage, this will be overridden. - optionalChains.set(result, true); - } - } else { - // Mark as required. + // Prefer optional chaining if any usage needs it, because dependency + // arrays are evaluated outside guarded blocks. + optionalChains.set(result, true); + } else if (!optionalChains.has(result)) { + // Mark as required unless an optional usage has already been seen. optionalChains.set(result, false); } }