Published: September 17, 2026 · by Srinu Desetti · facebook/react#37618 (opens in a new tab) · merged September 16, 2026
Guarding a React Compiler Fix with a Regression Test
React needs no introduction. My contribution — merged into facebook/react — protects a React Compiler fix for a useEffectEvent stale-value bug. The interesting part: when I investigated, the bug was already fixed — but completely unguarded. Nothing stopped it from silently coming back. My PR adds the guard.

The Promise useEffectEvent Makes
useEffectEvent is a React hook with exactly one guarantee: the callback you give it always reads the latest value — never a stale one. That's its entire reason to exist. If it can serve an old value, the hook is broken.
The Bug (Issue #37209)
Someone found a case where React Compiler broke that promise. Here is the actual reproduction from issue #37209 (opens in a new tab):
Walk through what should happen:
- The component mounts with
data = [], solengthis0. - The effect runs once (empty dependency array): it starts a mock fetch and registers
logLengthas a click listener. - After 300ms the fetch resolves —
databecomes[1, 2], solengthis now2. - You click.
logLengthis auseEffectEventcallback, so it must read the latestlength— the console should loglength 2.
What actually happened with React Compiler: clicking kept logging length 0 — the value from the first render, forever. ❌
Two details make this reproduction sharp:
lengthis declared after theuseEffectEventcall. That ordering is legal (the callback only runs later, on click), but it's exactly the shape that tripped the compiler's analysis — it memoized the callback with the first render's value baked in.- The listener is registered once and never re-registered. That's the whole point of
useEffectEvent: the effect shouldn't needlengthin its dependencies, because the event callback is supposed to see fresh values on its own. When the compiler freezes the callback, there's no re-registration to rescue you — the stale value is permanent.
What I Found: Fixed, But Unguarded
I took the exact code from the issue, turned it into a test fixture, and ran it against the compiler on main.
The bug didn't reproduce. The root cause had already been fixed — the compiler now leaves the useEffectEvent callback unmemoized, so it always reads the latest value.
But here's the problem: no test had been added when the fix landed. The scenario had zero coverage. That's the most dangerous state a bug can be in:
Reported → Fixed → Unprotected
↓
any future refactor can silently reintroduce it,
and nobody notices until users report it — againThe original issue existed precisely because users hit it in the wild. Without a test, the next regression would follow the same path: ship → break users → wait for a report.
What I Did
React Compiler tests work as fixtures: a source file the compiler transforms, plus the expected output. I added the issue's reproduction as a permanent fixture pair:
- Took the exact reproduction from issue #37209 and turned it into a compiler test fixture.
- Verified it exercises the previously-buggy path — a
useEffectEventcallback reading a variable declared later in the component. - Confirmed the expected output shows the callback left unmemoized (the fixed behavior).
- Now, if any future compiler change reintroduces the stale-value behavior, CI fails immediately and the change can't merge.
Honest scope: I did not write or change the compiler fix itself — no compiler logic was touched. The contribution is the guard: before my PR this scenario had zero test coverage; after it, the test suite watches it forever. A React maintainer (javache) reviewed and merged it, with all 25 CI checks passing.
Why a Test-Only PR Is a Real Contribution
This is the same lesson from my Tailwind CSS contribution, seen from the other side:
- In Tailwind, a test existed but never reached the buggy code path — it passed for the wrong reason, and the bug shipped.
- In React, the fix existed but no test reached it — one refactor away from shipping the bug again.
Both come down to the same principle: a fix without a test is a temporary fix. The codebase doesn't remember why code is shaped the way it is — tests are that memory. Locking a user-reported bug into CI is how it stays fixed.
Impact
- The
useEffectEventstale-value scenario from issue #37209 can never silently regress — any change that reintroduces it fails CI before merging. - A user-reported bug became institutional knowledge: the reproduction now lives in React's own test suite instead of an issue thread.
- Zero risk: the PR adds a fixture and its expected output — no behavior change for any React user.
What I Contributed
- Investigated issue #37209 against
mainand identified its true state: fixed but unprotected. - Converted the issue's reproduction into a compiler test fixture with its expected output.
- Verified the fixture exercises the previously-buggy path and locks in the unmemoized-callback behavior.
- Merged into facebook/react after review by a React maintainer, with all 25 CI checks passing.
Linked issue: facebook/react#37209 (opens in a new tab)
View the pull request → facebook/react#37618 (opens in a new tab)
← Previous: Node.js fs.glob fix · Next: Axios circular reference fix →
Frequently Asked Questions
What does useEffectEvent guarantee?
One thing: the callback you pass it always reads the latest values when it runs — never stale ones captured earlier. It exists so effect logic can read fresh props and state without re-running the effect. If it can serve an old value, the hook's contract is broken.
If the bug was already fixed, what did this PR actually do?
It added the missing protection. The fix had landed without any test covering the scenario, so any future compiler refactor could silently reintroduce the bug. The PR turns the issue's exact reproduction into a permanent regression test — now CI fails immediately if the behavior ever comes back.
What is a compiler test fixture?
A pair of files: an input source file the compiler transforms, and the expected compiled output. The test suite runs the compiler on the input and compares against the expected output. For this fixture, the expected output shows the useEffectEvent callback left unmemoized — the behavior that keeps values fresh.
Did this change any React or compiler behavior?
No. No compiler logic was touched — the PR adds one test fixture and its expected output. Its value is durability: the previously untested scenario is now watched by the test suite forever.