From e02c173fa597ce914c6876f71556c39bf20f40dc Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 23 Sep 2025 16:56:09 -0400 Subject: [PATCH] [lint] Allow useEffectEvent in useLayoutEffect and useInsertionEffect (#34492) --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34492). * #34497 * __->__ #34492 --- .../__tests__/ESLintRulesOfHooks-test.js | 66 +++++++++++++++++++ .../src/rules/RulesOfHooks.ts | 6 +- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 8d8040bb43cd1..2f3e14c5f95e4 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -1430,6 +1430,72 @@ if (__EXPERIMENTAL__) { } `, }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be called in useLayoutEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useLayoutEffect(() => { + onClick(); + }); + React.useLayoutEffect(() => { + onClick(); + }); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be called in useInsertionEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + useInsertionEffect(() => { + onClick(); + }); + React.useInsertionEffect(() => { + onClick(); + }); + } + `, + }, + { + code: normalizeIndent` + // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect + // and useInsertionEffect. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + const onClick2 = useEffectEvent(() => { + debounce(onClick); + debounce(() => onClick()); + debounce(() => { onClick() }); + deboucne(() => debounce(onClick)); + }); + useLayoutEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + React.useLayoutEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + useInsertionEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + React.useInsertionEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + return null; + } + `, + }, ]; allTests.invalid = [ ...allTests.invalid, diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index 0721a75e00642..4c7618d8e084c 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -147,8 +147,8 @@ function getNodeWithoutReactNamespace( return node; } -function isUseEffectIdentifier(node: Node): boolean { - return node.type === 'Identifier' && node.name === 'useEffect'; +function isEffectIdentifier(node: Node): boolean { + return node.type === 'Identifier' && (node.name === 'useEffect' || node.name === 'useLayoutEffect' || node.name === 'useInsertionEffect'); } function isUseEffectEventIdentifier(node: Node): boolean { if (__EXPERIMENTAL__) { @@ -726,7 +726,7 @@ const rule = { // Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent` const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee); if ( - (isUseEffectIdentifier(nodeWithoutNamespace) || + (isEffectIdentifier(nodeWithoutNamespace) || isUseEffectEventIdentifier(nodeWithoutNamespace)) && node.arguments.length > 0 ) {