From 473ab47a0c40113603c36fb788f3c19108d751ad Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Tue, 5 May 2026 19:14:34 +0530 Subject: [PATCH] Allow refs in IntersectionObserver callbacks --- .../src/HIR/Globals.ts | 16 +++++ .../Validation/ValidateNoRefAccessInRender.ts | 5 +- .../ReactCompilerRuleTypescript-test.ts | 58 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index faf7c9f2b72b..80f2aa4886bc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -620,6 +620,22 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ true, ), ], + [ + 'IntersectionObserver', + addFunction( + DEFAULT_SHAPES, + [], + { + positionalParams: [Effect.Freeze, Effect.Read], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInObjectId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + }, + null, + true, + ), + ], // TODO: rest of Global objects ]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts index 7da564205475..4467a682187c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -455,11 +455,14 @@ function validateNoRefAccessInRenderImpl( break; } case 'MethodCall': + case 'NewExpression': case 'CallExpression': { const callee = instr.value.kind === 'CallExpression' ? instr.value.callee - : instr.value.property; + : instr.value.kind === 'NewExpression' + ? instr.value.callee + : instr.value.property; const hookKind = getHookKindForType(fn.env, callee.identifier.type); let returnType: RefAccessType = {kind: 'None'}; const fnType = env.get(callee.identifier.id); diff --git a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts index a0d0f6bdbc8e..eddfb358ce52 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts +++ b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts @@ -195,7 +195,65 @@ const tests: CompilerTestCases = { ], }; +const refsTests: CompilerTestCases = { + valid: [ + { + name: 'Allows ref-reading callbacks passed to IntersectionObserver', + filename: 'test.tsx', + code: normalizeIndent` + import {useCallback, useMemo, useRef} from 'react'; + + type IntersectionCallback = (isIntersecting: boolean) => void; + + function useIntersectionObserver( + options: Partial, + ) { + const callbacks = useRef(new Map()); + + const onIntersect = useCallback( + (entries: ReadonlyArray) => { + entries.forEach(entry => + callbacks.current.get(entry.target.id)?.(entry.isIntersecting), + ); + }, + [], + ); + + return useMemo( + () => new IntersectionObserver(onIntersect, options), + [onIntersect, options], + ); + } + `, + }, + ], + invalid: [ + { + name: 'Reports ref-reading callbacks passed to unknown constructors', + filename: 'test.tsx', + code: normalizeIndent` + import {useCallback, useMemo, useRef} from 'react'; + + function useUnknownObserver(Ctor) { + const ref = useRef(null); + const onChange = useCallback(() => { + return ref.current; + }, []); + + return useMemo(() => new Ctor(onChange), [Ctor, onChange]); + } + `, + errors: [ + { + message: /Cannot access refs during render/, + }, + ], + }, + ], +}; + const eslintTester = new ESLintTesterV8({ parser: require.resolve('@typescript-eslint/parser-v5'), }); eslintTester.run('react-compiler', allRules['immutability'].rule, tests); +eslintTester.run('react-compiler refs', allRules['refs'].rule, refsTests);