@samitouri / QOS-React / commits / 008a6d4aeb

[compiler] Don't emit spurious `import { c as _c }` for discarded functions (#36500)

## What Codegen registers `_c` (the memo cache import) as a side effect whenever a function compiles with memo slots. The registration persists on `ProgramContext.imports` even if the function is later discarded (`'use no forget'`, `'use no memo'`, lint mode, validation errors). If other applied functions in the file compile to 0 memo slots, the stale `import { c as _c } from "react/compiler-runtime";` leaks into the output. ## Fix In `applyCompiledFunctions`, drop the memo cache import if no applied function uses memo slots. If `react/compiler-runtime` has no remaining specifiers, drop the module entry too so we don't emit a bare `import "react/compiler-runtime";`. ## Reproducer `use-no-forget-multiple-with-eslint-suppression.js`: ```js import {useRef} from 'react'; const useControllableState = options => {}; function NoopComponent() {} function Component() { 'use no forget'; const ref = useRef(null); // eslint-disable-next-line react-hooks/rules-of-hooks ref.current = 'bad'; return <button ref={ref} />; } ``` `NoopComponent` applies with 0 memo slots. `Component` is opted out, but codegen already registered `_c` for it. Before: ```js import { c as _c } from "react/compiler-runtime"; import { useRef } from "react"; ``` After: ```js import { useRef } from "react"; ``` ## Prior art TS counterpart to the Rust port's fix in 7e26eb89fc3264a34a9f03a68ac7a870d9f74b51. That commit also added `no-cache-slots-no-import.js`, which codifies the "no memo slots, no import" rule. ## Test plan - `yarn snap`: 1719/1719 passing - Snapshot for `use-no-forget-multiple-with-eslint-suppression` loses its spurious `_c` import - `no-cache-slots-no-import` still passes

lauren committed May 20, 2026 at 21:57 UTC 008a6d4aeba7a1d93c097f8978b5bc657a117231
3 files changed +18 -1
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts
+11
@@ -151,6 +151,17 @@ export class ProgramContext {
151 );
152 }
153
154 + removeMemoCacheImport(): void {
155 + const moduleImports = this.imports.get(this.reactRuntimeModule);
156 + if (moduleImports == null) {
157 + return;
158 + }
159 + moduleImports.delete('c');
160 + if (moduleImports.size === 0) {
161 + this.imports.delete(this.reactRuntimeModule);
162 + }
163 + }
164 +
165 /**
166 *
167 * @param externalFunction
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+7
@@ -775,6 +775,13 @@ function applyCompiledFunctions(
775
776 // Forget compiled the component, we need to update existing imports of useMemoCache
777 if (compiledFns.length > 0) {
778 + // Codegen may have registered `_c` for a function that was later discarded.
779 + const anyAppliedUsesMemo = compiledFns.some(
780 + result => result.compiledFn.memoSlotsUsed > 0,
781 + );
782 + if (!anyAppliedUsesMemo) {
783 + programContext.removeMemoCacheImport();
784 + }
785 addImportsToProgram(program, programContext);
786 }
787 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-forget-multiple-with-eslint-suppression.expect.md
-1
@@ -25,7 +25,6 @@ export const FIXTURE_ENTRYPOINT = {
25 ## Code
26
27 ```javascript
28 -import { c as _c } from "react/compiler-runtime";
28 import { useRef } from "react";
29
30 const useControllableState = (options) => {};