diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 1690f20ede490..7cb3ef0495341 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -1324,6 +1324,34 @@ const allTests = { `, errors: [asyncComponentHookError('use')], }, + { + code: normalizeIndent` + function App({p1, p2}) { + try { + use(p1); + } catch (error) { + console.error(error); + } + use(p2); + return
App
; + } + `, + errors: [tryCatchUseError('use')], + }, + { + code: normalizeIndent` + function App({p1, p2}) { + try { + doSomething(); + } catch { + use(p1); + } + use(p2); + return
App
; + } + `, + errors: [tryCatchUseError('use')], + }, ], }; @@ -1383,7 +1411,7 @@ if (__EXPERIMENTAL__) { const onEvent = useEffectEvent((text) => { console.log(text); }); - + useEffect(() => { onEvent('Hello world'); }); @@ -1421,7 +1449,7 @@ if (__EXPERIMENTAL__) { }); return onClick()} /> } - + // The useEffectEvent function shares an identifier name with the above function MyLastComponent({theme}) { const onClick = useEffectEvent(() => { @@ -1573,6 +1601,12 @@ function asyncComponentHookError(fn) { }; } +function tryCatchUseError(fn) { + return { + message: `React Hook "${fn}" cannot be called in a try/catch block.`, + }; +} + // For easier local testing if (!process.env.CI) { let only = []; diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index 8d42b319b4976..f0a2ffbda9e18 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -7,7 +7,13 @@ /* eslint-disable no-for-of-loops/no-for-of-loops */ import type {Rule, Scope} from 'eslint'; -import type {CallExpression, DoWhileStatement, Node} from 'estree'; +import type { + CallExpression, + CatchClause, + DoWhileStatement, + Node, + TryStatement, +} from 'estree'; // @ts-expect-error untyped module import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer'; @@ -111,6 +117,18 @@ function isInsideDoWhileLoop(node: Node | undefined): node is DoWhileStatement { return false; } +function isInsideTryCatch( + node: Node | undefined, +): node is TryStatement | CatchClause { + while (node) { + if (node.type === 'TryStatement' || node.type === 'CatchClause') { + return true; + } + node = node.parent; + } + return false; +} + function isUseEffectEventIdentifier(node: Node): boolean { if (__EXPERIMENTAL__) { return node.type === 'Identifier' && node.name === 'useEffectEvent'; @@ -532,6 +550,16 @@ const rule = { continue; } + // Report an error if use() is called inside try/catch. + if (isUseIdentifier(hook) && isInsideTryCatch(hook)) { + context.report({ + node: hook, + message: `React Hook "${getSourceCode().getText( + hook, + )}" cannot be called in a try/catch block.`, + }); + } + // Report an error if a hook may be called more then once. // `use(...)` can be called in loops. if (