`react-hooks/set-state-in-effect` flagged setState calls that happen
after an `await` inside an async function invoked from an effect.
Post-await code resumes in a microtask after the effect body has
returned, so the "synchronous setState cascades a render" rationale does
not apply; the lint was a false positive on a common data-loading shape
(15 reactions on the issue).
The fix exempts a setState only when it is *provably* post-await: a
forward must-dataflow over the HIR CFG computes the blocks that begin
after an await has executed on every path from the function entry
(optimistic initialization so loop back-edges do not pessimize the meet,
fixpoint to the greatest solution), plus an intra-block flag for
instructions after an Await in the same block. A setState reachable on
any await-free path still flags, so the conditional-await case remains
an error by design, with a fixture documenting that choice. Suppression
is sound under try/catch because HIRBuilder terminates blocks after each
instruction in a try region, and an awaited rejection also resumes in a
microtask.
Fixtures: post-await setState (event gone), setState before the first
await (still flags), setState after a conditional await (still flags).
First commit documents the false positive via the lint-mode logger
output, second removes it.
Builds on the approach in #36417 by @raashish1601, hardened from a
seen-await flag to the path-sensitive analysis above. Implemented
identically in the TypeScript compiler and the Rust port.
Verification: TS snap 1807/1807, Rust snap 1807/1807, cargo workspace
green, scoped TS-vs-Rust HIR parity harness green.
Closes#34905
---------
Co-authored-by: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com>
Before this would incorrectly mark certain computed properties as
shorthand. The babel code didn't care about this because of how it was
written, but the swc short circuited on shorthand. Causing incorrect
code generation
The Flight client dropped a leading `U+FEFF` from outlined text rows
when it decoded binary input. `TextDecoder` consumed the character as an
encoding signature, even though it was part of the serialized string.
Both the Node and Web decoders now use `ignoreBOM: true` to preserve it.
Regression tests cover one and two leading U+FEFF characters with normal
chunks and with every UTF-8 byte delivered separately. They also include
an inline-string control. The global `TextDecoder` Flow declaration now
makes `fatal` optional and declares `ignoreBOM`, matching the API.
Before this change, the compiler would incorrectly memoize the arguments
object. Causing rerenders to not work properly. I looked into adding
proper support, but it seemed it would require a decent amount of change
to do it cleanly, so I figured getting rid of the miscompilation was a
good start.
We had tests that were using it for compiler purposes, from what I can
tell changing to ...args kept the intent, so I moved them to that.
Added behind a new experimental flag, `enableFlightObjectReferences`.
Extends Server References so that a reference can point to an object,
not just a function. An object is registered with a new API,
`registerServerObjectReference`, which tags it with its module id the
same way `registerServerReference` tags a Server Function.
The client receives an opaque handle: reading a property or calling it
throws, similar to how Temporary References work inside Server
Functions. The only thing the client can do with the handle is pass it
back to the server via a Server Function, where it resolves through the
server manifest.
The motivating use case is letting a framework pass request-scoped
values by reference, so it can omit them from the request body. For
example, a framework could model `searchParams` as a module export whose
value resolves from the current request. This avoids encoding the search
params twice (they already appear in the request URL) but it also makes
the responses more cacheable: if the search params don't appear
elsewhere in the body, then a response cache can omit them from its
cache key.
This initial PR only exposes the new API in the Turbopack bindings. We
will port it to the other bundler configs if the experiment advances.
## Summary
The Rust port's constant propagation folds `**` with `f64::powf`, which
follows IEEE 754. IEEE `pow` returns `1` for `pow(1, NaN)` and `pow(±1,
±∞)`, but ECMAScript's
[`Number::exponentiate`](https://tc39.es/ecma262/#sec-numeric-types-number-exponentiate)
returns `NaN` in those cases. The TS compiler folds with native `lhs **
rhs`, so the two backends disagree, and the Rust-compiled output changes
runtime behavior:
```js
const x = 1 ** (1 / 0); // JS: NaN, Rust-compiled: 1
```
This adds `js_exponentiate`, which returns `NaN` when `|base| == 1` and
the exponent is not finite, and otherwise defers to `powf`. `**=` lowers
to the same `BinaryOperator::Exponent`, so it is covered too.
To check the guard is neither too narrow nor too broad, I compared
`f64::powf` against JS `**` over all 225 pairs drawn from {NaN,
±Infinity, ±0, ±1, ±2, ±0.5, ±3, ±1e308}. The only mismatches were `1 **
NaN`, `1 ** ±Infinity` and `(-1) ** ±Infinity`. The condition also
matches `(-1) ** NaN`, where `powf` already returns `NaN`.
While in this file I noticed two places where `js_to_number` diverges
from `ToNumber`. I left them out to keep this PR focused, and can send a
follow-up if that's useful:
- `trimmed.parse::<f64>()` accepts `"inf"`, `"infinity"` and
`"INFINITY"`, so `"inf" == 1 / 0` folds to `true` (JS: `false`).
- The `0x`/`0o`/`0b` branches use `u64::from_str_radix`, which fails
past 64 bits (`"0xFFFFFFFFFFFFFFFFF" == 295147905179352830000` folds to
`false`, JS: `true`) and accepts a sign after the prefix (`"0x+10" ==
16` folds to `true`, JS: `false`).
## How did you test this change?
Added `constant-propagation-exponent-non-finite.js` with the five
mismatching cases plus five controls: `(-1) ** NaN`, `NaN ** 0`, `2 **
Infinity`, `0.5 ** Infinity` and `2 ** 10`. The snapshot was generated
with the TS compiler.
- Before the fix, `yarn snap --rust -p
constant-propagation-exponent-non-finite` fails on eval output: expected
`[null,null,null,null,null,null,1,null,0,1024]`, got
`[1,1,1,1,1,null,1,null,0,1024]`.
- With a narrower guard (`exponent.is_infinite()` instead of
`!exponent.is_finite()`) it still fails, only on `1 ** NaN`.
- With the fix, it passes on both backends.
I also ran the steps from `compiler_rust.yml` locally:
- `cargo check`, `cargo build`, `cargo test --workspace` (49 passed)
- `bash scripts/test-babel-ast.sh`
- `bash scripts/test-rust-port.sh`: 1816 passed, 0 failed (1815 on
`main`, before the new fixture)
- `yarn snap --rust` and `yarn snap`: 1817 passed, 0 failed
Fixes#37209
The stale value from the issue does not reproduce on main anymore.
The compiler leaves the useEffectEvent callback unmemoized, so it
always reads the latest value. I added the repro from the issue as
a test fixture so this stays covered.
The new fixture passes with yarn snap.
Available since Node.js 14
Fixes
```
● ReactFlightDOMNode › detaches the abort listener from a composite signal once the prerender completes
TypeError: signals[0] is not of type AbortSignal.
2490 | const outer = new AbortController();
2491 | const timeout = new AbortController();
> 2492 | const composite = AbortSignal.any([outer.signal, timeout.signal]);
| ^
2493 |
2494 | function App() {
2495 | return <div>hello world</div>;
at Object.<anonymous> (packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js:2492:33)
```
in modern Node.js (e.g. 24.20.0).
## Summary
`react-hooks/purity` already reports `Date.now()` during render, but not
`new Date()` / `new Date().getTime()` / `new Date().getFullYear()`.
Those also read the current clock. React's purity docs list `new Date()`
next to `Date.now()`.
Zero-argument `new Date()` is now treated as an impure call (same
diagnostic as `Date.now()`). Constructing from an explicit timestamp
stays allowed: `new Date(timestamp)`.
Fixes#37553
## How did you test this change?
- `yarn snap --pattern "**/*new-date*"` (4 fixtures passed)
- `yarn workspace eslint-plugin-react-compiler test --
ImpureFunctionCallsRule` (4 tests passed)
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Pieter De Baets <pieter.debaets@gmail.com>
Backports https://github.com/oxc-project/oxc/pull/26451 to the Rust
React Compiler.
Check component parameters before scanning for hooks or JSX, so
functions with invalid component signatures skip the traversal.
Component classification stays the same.
Co-authored-by: Codex <noreply@openai.com>
## Summary
`FragmentInstance.compareDocumentPosition()` returns
`DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC` instead of `CONTAINS |
PRECEDING` for the container passed to `createRoot()`, and for any
element between that container and `document.body`:
```js
const container = document.getElementById('root');
createRoot(container).render(<div><Fragment ref={ref}><div /></Fragment></div>);
ref.current.compareDocumentPosition(document.body); // contains
ref.current.compareDocumentPosition(container); // implementationSpecific
```
The `CONTAINS` fiber validation enumerates the nodes that have no fiber
as `document`, `documentElement` and `body`. That list is never
complete: the root container has no fiber either, and neither does any
non-React element above it, so both fall through and are reported as
implementation specific.
This checks that `otherNode` contains the root container instead, which
covers all of those uniformly.
Adjacent to #37579, which fixed a `null` dereference in the same branch.
## Summary
Fixes#22718.
**Root cause:** `toggle` and `beforetoggle` (fired by `<details>`,
`<dialog>`, and the Popover API) don't bubble natively in the DOM, and
they were already listed in `nonDelegatedEvents` (native listener
attached directly to the target). However, `SimpleEventPlugin`'s
`accumulateTargetOnly` check — which controls whether React accumulates
listeners for just the target vs. the whole ancestor chain — only
special-cased `scroll`/`scrollend`. So `toggle`/`beforetoggle` still had
their listeners accumulated up the fiber tree, meaning ancestor
`onToggle`/`onBeforeToggle` handlers could fire even though the native
event never reached them.
**Fix:** Extend the existing `accumulateTargetOnly` condition in
`packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js` to
also cover `toggle` and `beforetoggle`, aligning them with the
scroll/scrollend non-bubbling precedent. No other files touched.
**Tests:** Moved the existing `onToggle`/`onBeforeToggle` test cases
(plain `<details>`, Popover API, Dialog API) in
`ReactDOMEventPropagation-test.js` from the "non-bubbling events that
bubble in React" describe block (`testEmulatedBubblingEvent`) into
"non-bubbling events that do not bubble in React"
(`testNonBubblingEvent`), matching how `onScroll`/`onScrollEnd` are
tested. Also added a small `...eventConfig.targetProps` spread to the
`testNonBubblingEvent*` helpers so `targetProps` (e.g. `popover: 'any'`)
reach the fixture, mirroring the `testEmulatedBubblingEvent*` helpers.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
## Summary
V8 prefixes async call sites with `async ` when it prints a stack, like
this:
```
Error: boom
at inner (/tmp/asy.js:1:44)
at async outerName (/tmp/asy.js:2:30)
```
`parseStackTraceFromChromeStack` captures `async outerName` as the frame
name and strips the prefix here:
```js
} else if (name.startsWith('async ')) {
name = name.slice(5);
isAsync = true;
}
```
`'async '` is six characters, so `slice(5)` leaves the space behind and
the frame name comes back as `' outerName'` rather than `'outerName'`.
I noticed it while reading the parser, and it is not purely cosmetic:
that name is the first element of the `ReactFunctionLocation` returned
by `extractLocationFromComponentStack` and
`extractLocationFromOwnerStack`, which `backend/fiber/renderer.js`
stores as `instance.source`. Any async component whose frame reaches
that path is recorded under a name with a stray leading space.
The same line exists in
`packages/react-server/src/ReactFlightStackConfigV8.js`, which the
DevTools file is a copy of. After review I fixed it in this PR as well,
in a second commit. There it only matters on the fallback path that
parses an already formatted stack string, when the error's `stack` was
read or assigned before React reaches it. #37130 is open on that file
too, but it does not touch these lines.
## How did you test this change?
I first confirmed the format V8 actually emits, rather than assuming it:
```
$ node -e 'async function inner(){await null;throw new Error("boom")}
async function outerName(){await inner()}
outerName().catch(e=>console.log(e.stack))'
Error: boom
at inner ([eval]:1:52)
at async outerName ([eval]:2:33)
```
Then I added a case to the existing `extractLocationFromComponentStack`
block in `utils-test.js`. Against `main` it fails with exactly the
leading space:
```
● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name
- Expected - 1
+ Received + 1
Array [
- "Comments",
+ " Comments",
"https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js",
5,
9236,
]
```
With the one-character fix applied:
```
$ yarn test --build --project=devtools -r=experimental utils-test
PASS packages/react-devtools-shared/src/__tests__/utils-test.js
Tests: 63 passed, 63 total
```
I also ran the whole DevTools project before and after to check I was
not moving anything else. Both runs end at `9 failed, 4 failed suites`,
the same test names each time (`componentStacks`, `console`,
`inspectedElement`, `legacy/inspectElement`), so those failures are
pre-existing on `main` in my environment and unrelated to this change.
The only difference between the two runs is my new test: 586 passed
before, 587 after.
For the Flight side I added `ReactFlightStackConfigV8-test.js`, which
assigns a formatted stack to an error and checks what `parseStackTrace`
returns. Against `main` it fails with the same leading space (`"
outerName"`); with the fix it passes on stable and experimental in
development. It is gated to `__DEV__` because that fallback goes through
the DEV-only stack cache. `ReactFlightServer-test` and
`ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests).
`prettier` and `eslint` are clean on all changed files. `yarn flow
dom-node` reported no errors for the DevTools commit; I did not rerun
Flow after the one-character Flight change.
AI tools used
## Summary
AI Disclosure: This was assisted using Claude Code, Fable 5.1. This PR
description was fully written by me, manually, and the code was reviewed
and tested by me manually.
This improves the accuracy of the animation-related event props
supported by React.
This adds `onAnimationCancel` alongside the existing `onAnimationStart`,
`onAnimationIteration`, and `onAnimationEnd` props.
Adds `onAnimationCancel` to React DOM, completing the animation event
family alongside `onAnimationStart`, `onAnimationIteration`, and
`onAnimationEnd`.
[`animationcancel`](https://developer.mozilla.org/en-US/docs/Web/API/Element/animationcancel_event)
is a CSS Animations Level 1 event fired when an animation stops before
it has completed.
This changeset is based on the similar changes in #27345 for the
transition events.
Note that MDN's browser compat data [currently
claims](https://developer.mozilla.org/en-US/docs/Web/API/Element/animationcancel_event)
that this is not supported in Chrome or Edge (at least not as
`onanimationcancel`), but this change will work in Chrome 83+ due to
React passing things through `addEventListener` anyway, and also the MDN
docs are [outdated about
this](https://github.com/mdn/browser-compat-data/issues/29376), Chrome
has started supporting `onanimationcancel` directly [since earlier this
year](https://issues.chromium.org/issues/41404325).
Also note that the animation and transition events on DOMEventName can
probably be made to no longer do this vendor prefixing dance? I didn't
do that in this PR and it should be done separately, but they've not
been prefixed in a long time now and the comment rationalizing the
current code mentions Android 4.x, which was last released over a decade
ago now. So it seems like something that has just never been cleaned up.
## How did you test this change?
- Added tests to match the sibling properties' tests.
- Ran `yarn test`, `yarn lint`, and `yarn prettier` and all passed.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
## Summary
Fixes#37578.
`FragmentInstance.compareDocumentPosition(document)` threw:
```
TypeError: Cannot read properties of null (reading 'documentElement')
```
because `Document.ownerDocument` is `null`, and the `CONTAINS`
fiber-validation fallback read `ownerDocument.documentElement`
unconditionally. `document.body` / `document.documentElement` already
worked.
This treats a `Document` node as its own owner document, matching
`Node.compareDocumentPosition(document)`.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
> [!NOTE]
> Stacked on #37573. That PR's commit shows up in the diff here until it
lands; only the second commit is part of this change.
## Summary
Inlines the enabled branch of `enableFragmentRefsScrollIntoView` and
removes the flag from `ReactFeatureFlags` and all of its forks.
This is a DOM-only change. The flag's only consumer was
`ReactFiberConfigDOM`, where it guarded the definition of
`FragmentInstance.prototype.scrollIntoView`, so the code change is a
plain unwrap and dedent of that one block with no logic edits.
The flag was declared `false` in the native forks, but those
declarations were inert: Fabric never defined `scrollIntoView` on
`FragmentInstance` at all, so removing the flag does not change any
native behavior.
The one combined gate, `@gate enableFragmentRefsTextNodes &&
enableFragmentRefsScrollIntoView`, was reduced to `@gate
enableFragmentRefsTextNodes` rather than dropped.
## How did you test this change?
`yarn lint`, `yarn prettier-check`, and `yarn flow` for `dom-node`,
`dom-browser`, and `fabric` all pass.
Full test suite run across `experimental`, `stable`, `www-modern`,
`www-classic`, and `xplat`, with both `--variant` settings, plus
`--persistent`. The remaining failures (Fizz / Flight / FrameScheduling
/ ClassEquivalence) are pre-existing: I diffed the individual failing
test names against the parent commit and they are identical.
## Summary
- support prefix and postfix update expressions on captured variables in
both TypeScript and Rust compiler backends
- represent updates with explicit `PrefixUpdateLocal`,
`PrefixUpdateContext`, `PostfixUpdateLocal`, and `PostfixUpdateContext`
HIR variants
- model captured updates as mutations so SSA, effect inference,
dead-code elimination, and post-render validation preserve their
semantics
- convert the existing TODO fixtures to passing coverage and add the
Airwave-shaped map regression from T286959188
- correct Rust workspace manifest paths in the rust-port documentation
## How did you test this change?
- `yarn prettier`
- `yarn linc`
- `yarn flow dom-node`
- `yarn test --silent --no-watchman React-hooks-arity`
- `yarn test-www --silent --no-watchman useMemoCache`
- `yarn test-www --variant=false --silent --no-watchman useMemoCache`
- `node_modules/.bin/tsc --noEmit -p
compiler/packages/babel-plugin-react-compiler/tsconfig.json`
- `cargo +1.93.1 fmt --manifest-path compiler/Cargo.toml --all --
--check`
- `cargo +1.93.1 check --manifest-path compiler/Cargo.toml -p
react_compiler -j 1`
- targeted Cargo tests for lowering, SSA, inference, optimization, and
validation
- TypeScript and Rust snapshot runs for `*update-expression*` (11 tests
each)
## Summary
`enableFragmentRefs` is enabled in every channel, so this inlines the
enabled branch and removes the flag from `ReactFeatureFlags` and all of
its forks.
Most of the diff is mechanical, but a few spots needed care:
- Several `case Fragment:` blocks in `ReactFiberCommitWork` had a `//
Fallthrough` that was only reachable with the flag off. Where a
preceding case falls *into* `Fragment` (the `ViewTransitionComponent`
cases), I verified the resulting behavior is unchanged for every
remaining flag combination.
- `commitAttachRef` in `ReactFiberCommitEffects` becomes a plain `case
Fragment: { ... break; }` instead of a conditional fallthrough into
`default`.
- The `React.Fragment` invalid-prop warning no longer has two variants;
it always mentions `key`, `ref`, and `children`.
The three related flags — `enableFragmentRefsScrollIntoView`,
`enableFragmentRefsInstanceHandles`, and `enableFragmentRefsTextNodes` —
are *not* on everywhere yet and are left in place.
In tests, `enableFragmentRefs` was stripped from 101 `@gate` pragmas.
Combined gates such as `@gate enableFragmentRefs &&
enableFragmentRefsTextNodes` were reduced rather than removed. One test
asserted the absence of a warning under the flag, so it was renamed from
`warns for fragments with refs` to `does not warn for fragments with
refs`.
## How did you test this change?
`yarn lint`, `yarn prettier-check`, and `yarn flow` for `dom-node`,
`dom-browser`, and `fabric` all pass.
Full test suite run across `experimental`, `stable`, `www-modern`,
`www-classic`, and `xplat`, with both `--variant` settings, plus
`--persistent`. The remaining failures (Fizz / Flight / FrameScheduling
/ ClassEquivalence) are pre-existing: I diffed the individual failing
test names against the base commit and they are identical.
## Summary
`get_identifier_name_with_loc` reads an identifier's name out of the
source when SSA has dropped it. `SourceLocation.index` is a Babel
position and counts UTF-16 code units, but the fallback used it to slice
a Rust `&str`, which indexes UTF-8 bytes:
```rust
let slice = &code[start_idx..end_idx];
```
These agree only while the source is ASCII. After any non-ASCII
character, later offsets are short by the extra bytes, so the slice
reads the wrong span, or panics when it lands inside a character:
```
panicked at crates/react_compiler_validation/src/validate_no_set_state_in_effects.rs:168:30:
start byte index 637 is not a char boundary; it is inside 'う' (bytes 636..639 of string)
```
The panic aborts validation for the whole file, so every diagnostic in
it is lost.
`validate_no_derived_computations_in_effects.rs` already converts
correctly in its own copy of this function. Both were added in #36173
and only one got the UTF-16 handling, so this ports that logic over.
Found through Biome, which embeds these crates for its
`useReactCompiler` rule.
## How did you test this change?
Added `repro-setState-in-effect-non-ascii-source.ts`, which panics
before this change and compiles cleanly after:
```
bash scripts/test-rust-port.sh ValidateNoSetStateInEffects \
packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-setState-in-effect-non-ascii-source.ts
```
The non-ASCII comments in it are test input, not documentation; removing
them realigns the offsets and the crash disappears.
Everything `compiler_rust.yml` runs is green on macOS arm64, including
`scripts/test-rust-port.sh` (1811 passed) and `yarn snap --rust` (1812
passed).
## Summary
- Bump React DevTools from 7.0.1 to 8.0.0 (packages + extension
manifests only; no publish).
- `scripts/devtools/prepare-release.js` only supports minor/patch, so
this major bump is manual.
- Changelog is curated from DevTools commits since 7.0.1 (features vs
bugfixes; internal/test-only changes omitted).
## Test plan
- [ ] Confirm published package versions would be 8.0.0 for
`react-devtools`, `react-devtools-core`, and `react-devtools-inline`
- [ ] Confirm Chrome/Edge/Firefox extension manifests show 8.0.0
- [ ] Review `packages/react-devtools/CHANGELOG.md` 8.0.0 section for
accuracy before undrafting
## Summary
- Add a manual `workflow_dispatch` workflow that publishes only
`react-devtools-cdt-mcp`.
- Match the runtime release security model: empty default permissions,
checkout of `github.sha` only, protected `npm` environment, Node 24, and
OIDC trusted publishing (no `NPM_TOKEN`).
Stacked on https://github.com/react/react/pull/37500
## Test plan
- [ ] Open the workflow in GitHub Actions and confirm it is
`workflow_dispatch` only.
- [ ] Dry-run dispatch after the version bump lands, once npm trusted
publishing is configured for this package.
## Summary
- Bump `react-devtools-cdt-mcp` from the placeholder `0.0.0` to `0.1.0`.
- Describe the package as a browser library that registers React tools
with chrome-devtools-mcp.
Stacked on https://github.com/react/react/pull/37503
## Test plan
- [ ] Confirm `packages/react-devtools-cdt-mcp/package.json` is `0.1.0`
before publishing.
- [ ] Dry-run the publish workflow after this PR.
A row that is still parsing can hand its partially built value to
references that were registered on it during that parse.
`initializeModelChunk` fulfilled every such listener, on the assumption
that all of them are cyclic references back into the parsing row. Only
some are. A listener from a nested parse that is not part of a cycle
belongs to a handler that does not wait on the parsing row, so
fulfilling it early completes that handler with an object that still has
references outstanding.
When that handler owns an element, `initializeElement` runs on
incomplete props. In DEV the props are frozen, so the write that arrives
later throws `Cannot assign to read only property`, and
`rejectReference` escalates the error into the rows that wait on the
element, up to the root. Only the debug tree can produce this shape. The
RSC stream writes element props inline, while the debug channel outlines
a props object that an element shares with its own componentInfo into a
separate row, and that row can still wait on a client module.
`resolveBlockedCycle` already tells a cyclic reference from any other,
but it returned `null` for mid-parse listeners because `handler.chunk`
was assigned after the drain loop. This change assigns it before the
loop and classifies each listener. A reference whose handler is
transitively waiting on the parsing row is a genuine cycle and receives
the value now, because neither side can complete before the other. Every
other listener is queued back on the parsing row and fulfilled when that
row completes, like any reference into a blocked row.
A row whose own parse fails used to hand the partial value to its
mid-parse listeners before it threw. It now errors through
`triggerErrorOnChunk`, which rejects them the same way a reference into
any other errored row is rejected. The `if (handler.errored) throw`
after the loop is removed. It also caught a rejection during the loop,
which now reaches `triggerErrorOnChunk` on its own because
`handler.chunk` is set.
One behaviour changes beyond the reported bug. When
`initializeDebugChunk` errors a chunk before `parseModel` runs, the old
code set `INITIALIZED` over that status if the model had no pending
references, and left it `ERRORED` otherwise. The chunk now stays
`ERRORED` in both cases. That is what the `triggerErrorOnChunk` call in
`initializeDebugChunk` intends, and the TODO above the `parseModel` call
already notes that the chunk can be `ERRORED` there.
PR #37398 deferred `Object.freeze(element.props)` until the outstanding
references have resolved. That removes the exception but not the cause:
the element is still initialized on an incomplete object and is visible
through `_debugInfo` with a `null` placeholder until the late write
lands. With the early release fixed, the freeze needs no change.
**Alternatives Considered**
- Deferring every listener whose handler is not the parsing row's own
deadlocks `foo ↔ bar` in `can deduped outlined references inside
promises`. One side of a genuine cycle has to accept the partial object.
- Holding an element back while its props row is `BLOCKED` breaks
`should handle deduped props of re-used elements in fragments`, where
the row is blocked on an unrelated module and the props object itself is
complete.
- A per-object count of pending writes plus a reverse `dependents` edge
works, but adds a second dependency graph next to `deps` and
special-cases elements.
Fixes#37361Closes#37398
## Summary
- Rewrite the package README so npm consumers can tell this is a
page-side library, not an MCP server.
- Document the chrome-devtools-mcp 1.3.0+ requirement, the experimental
third-party flag, and the corrected DOM-tool output.
Stacked on https://github.com/react/react/pull/37497
## Test plan
- [ ] Read the README as a first-time installer and confirm install,
import order, and MCP config are clear.
- [ ] Confirm `react_get_component_by_dom_element` no longer documents
`hooks`.
Preserve the RefValue source location when joining mixed ref types so
validation errors point to the original ref access.
Before this change it used to show the incorrect location when using the
rust compiler.
```rust
2 | const ref = useRef(null);
3 | const x = cond ? ref : ref.current;
> 4 | return <Foo value={x} />;
| ^ Cannot access ref value during render
5 | }
6 |
```
## Summary
- Upgrade the cdt-mcp e2e dependency from chrome-devtools-mcp 1.3.0 to
1.8.0.
- Pass `pageId` to page-scoped CLI tools, which 1.8.0 requires.
- Use a hex `sessionId` (`crypto.randomUUID()`). 1.8.0 rejects ids that
are not `/[a-fA-F0-9-]+/`.
Stacked on https://github.com/react/react/pull/37496
## Test plan
- [ ] `yarn --cwd packages/react-devtools-cdt-mcp test:e2e`
## Summary
- `react_get_component_by_dom_element` returns the host node for a DOM
element, which never has hooks.
- Stop advertising `hooks?` in the tool description so agents do not
request a field that is never returned.
## Test plan
- [ ] `yarn test --build --project=devtools -r=experimental
DevToolsCdtMcp`
Propagate errors from block lowering instead of continuing with
incomplete HIR.
This prevents the Rust compiler from emitting partial output when it
encounters unsupported syntax, including nested TypeScript `this`
parameters.
This landed earlier as #37232, which exposed an existing FBT diagnostic
ordering issue. Resolve local FBT bindings before checking whether an
`<fbt>` tag comes from a module import, so the compiler reports the
earlier, more useful Todo instead of a later invariant.
Run from the repository root:
- `yarn --cwd compiler workspace babel-plugin-react-compiler-rust test`
- `cargo test --manifest-path compiler/Cargo.toml -p
react_compiler_lowering`
Apply the render's script nonce to import maps emitted through the
`importMap` server rendering option. This keeps configured import maps
compatible with nonce-based Content Security Policies and uses the same
escaped nonce value as other render-managed scripts.
The warning was previously only enabled for experimental builds
(`react@experimental`). This enables the warning for `react@canary` as
well.
Keep in mind that conditional `use()` is generally supported. This
warning only triggers if the condition is based on `promise.status` (or
`promise.value`). Let `use()` handle that status. React will not suspend
if the `promise.status` is already `'fulfilled'`.
More information can be found in the [`use()` docs under "Don’t skip
calling use based on whether a Promise is already
settled."](https://react.dev/reference/react/use#conditional-use).
We've tested this at Vercel on the latest version of SWR (which
previously had conditional `use()` calls) and found no false-positive
warnings or excessive warnings.
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
## Summary
`build-and-test.js` could feed three different commits into one release:
`git archive main` (not `HEAD`), an interactively chosen React CI build,
and `HEAD` saved as metadata. Firefox source review then could not
reproduce the zip.
Require a clean tree, resolve `HEAD` once, and use that hash for the
source archive, the experimental React download, and the metadata
printed for AMO. Drop the prompt that let those diverge.
Depends on #37305 and #37306.
## How did you test this change?
Build-script only. The release helper now errors on a dirty tree and
threads a single `git rev-parse HEAD` into archive, download, and
metadata.
Co-authored-by: Ruslan Lesiutin <hoxy@meta.com>
## Summary
The extension build writes `new Date().toLocaleDateString()` into
Chrome/Edge `version_name` and into every browser's manifest
description. That string changes with the calendar, timezone, and
locale, so a Firefox AMO rebuild on another day cannot match the
uploaded zip.
Stop stamping dates. `version_name` stays the value from the source
manifest (still updated by `prepare-release.js` on version bumps). The
description still records the commit from #37305.
Depends on #37305. Next: #37307.
## How did you test this change?
Build-script only. After this, `manifest.json` description is `Created
from revision <commit>.` and Chrome/Edge `version_name` is the committed
version string.
---------
Co-authored-by: Ruslan Lesiutin <hoxy@meta.com>
## Summary
`git show --format=%h` is not stable: abbreviation length depends on
`core.abbrev` and how unique the prefix is in that clone. Two rebuilds
of the same commit can therefore embed different strings in
`DEVTOOLS_VERSION` and the extension manifest.
Always take the full hash (`%H`) and slice it to 10 characters so the
value is the same everywhere.
This also removes the `build/COMMIT_SHA` fallback used when Mozilla
rebuilds from a git archive (no `.git`). That path used a different
length (7) and a different source, so it could not match a git checkout
of the same commit. Firefox source review should rebuild from a checkout
of the commit in #37307, not from the tarball alone.
Stack: this PR → #37306 → #37307.
## How did you test this change?
Build-script only. `getGitCommit()` now returns `HEAD` sliced to 10
chars, independent of `core.abbrev`.
Co-authored-by: Ruslan Lesiutin <hoxy@meta.com>
Sizebot compared the pull request head build against the build of
`pull_request.base.sha`, which is the tip of the base branch at event
time, not the commit the pull request diverged from. The field's
semantics are undocumented in GitHub's API schema (the OpenAPI
description types it as a bare string); the observed behavior and the
compare API's `merge_base_commit` confirm the difference.
The difference between `pull_request.base.sha` and the merge base is
confirmed with an example in https://github.com/react/react/pull/37356
The sizebot job now resolves the merge-base through the compare API and
downloads the base build for that commit instead, so the report only
ever contains the pull request's own changes. The job gains `contents:
read` for the compare call.
When no base build can be downloaded for the merge-base, for example
because its artifacts aged out of the retention window or its run
failed, the sizebot job records a `base-build-not-found` result instead
of failing immediately. `render-comment.js` on the default branch
renders that as a warning comment naming the base commit and writes the
`sizebot-problem.txt` marker, so the comment workflow fails its check
after posting the warning, the same pattern already used for build
configuration drift. The sizebot job itself intentionally stays green: a
failed run would make the renderer discard the results and mask the
warning with a generic "did not complete" message.
Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
tl;dr reduces memory churn by 7.8%, allocations by 4.1%, wall time by
~4%
## Summary
When compiled functions are written back to the AST,
`apply_compiled_functions` took a reference to a slice, and deep-cloned
each compiled body out of it. In theTS version this step assigns
references through Babel paths, which are essentially free. The Rust
port translated that as `.clone()` for safety, deep-copying the entire
codegen output for every compiled function.
Nothing needs these bodies after they are inserted, and the caller
already owns the vector. So this takes `compiled_fns` by value and moves
the data into the AST instead:
* `ReplaceFnVisitor` holds an `Option<CodegenFunction>` and moves it to
whatever arm matches
* Outlined function declarations move their id+params+body out of
`codegen_fn.outlined` rather than cloning them
* `needs_memo_import` is computed before the loop that consumes the
vector. Only the computation moved; the block that registers the import
stays where it was, so ordering and identifier numbering don't change.
This doesn't fully eliminate clones, just ones where it's easy to do a
move instead.
## How did you test this change?
All compiler fixtures pass with byte-identical output
97.6% of the value sets tracked per identifier in mutation / aliasing
inference hold exactly one element, but each was a `FxHashSet`, meaning
each was a heap allocation.
Because the inference code retains a full state in each basic block,
these single-element hashsets were a major contributor to peak memory
allocation.
This replaces them with a small inline set inspired by smolvec /
tinyvec. Five values are stored inline, and any more spill to the heap.
This was only needed in **0.02%** of sets in my data corpus.
This also makes iteration order match the TS implementation. TS uses
`Set` and iterates in insertion order; the Fx set iterated in hash
order. This brings the two behaviors in line.
| Benchmark | Peak allocation | Allocation count | Wall time |
|------------------|-----------------------------|------------------|-----------|
| legacy/image.tsx | 33.40 -> 28.07 MiB (-16.0%) | -66.7% | -28.8% |
| next-client | 33.40 -> 28.07 (-16.0%) | -42.0% | -15.7% |
| devtools | 16.29 -> 14.25 (-12.5%) | -19.9% | -7.3% |
| fixtures | 9.41 -> 7.90 (-16.0%) | -7.3% | -3.3% |
| next-examples | 4.85 -> 4.85 ( 0.0%) | -4.8% | -1.6% |
## Summary
Alternative to #37280 that keeps the child-set assertion and instead
fixes the root cause.
The assertion "The children should not have changed if we pass in the
same set." fired while DevTools reconciled the hidden content tree of a
Suspense boundary that had just switched to its fallback.
`updateSuspenseChildrenRecursively` reconciles the content and the
fallback in two passes, but the previous-set lockstep pointer of the
content pass is not bounded. When a boundary is suspended on both sides
of a commit, the pointer advances from the previous content Offscreen
onto the previous fallback fragment, and the leftover-children check
reports `ShouldResetChildren` even though the fallback is reconciled in
the second pass by design.
For a boundary that is filtered from the tree, that flag propagates to
the parent child list, freezes its lockstep pointer, forces the
following sibling to be paired by alternate, and the instance scan
(which only matches the paired previous fiber) no longer finds the
existing instance, since instances track the current fiber. The subtree
below is then walked without its instance, which cascades into spurious
unmount and remount work and surfaces at the assertion in the filtered
same-child-set branch.
We're also avoiding creation of new backend instances in those
scenarios.
This change bounds the previous set of the content pass by the previous
fallback fragment via a new `prevLastChild` parameter, so the flag
disappears.
Closes#37280
## How did you test this change?
- cherry-picked test from #37280
---------
Co-authored-by: Ruslan Lesiutin <hoxy@meta.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
## Summary
- configure the React Compiler Rust crates as a Cargo workspace with
shared package metadata and versioned internal dependencies
- add workflows to open a signed version-bump pull request and publish
the workspace through crates.io trusted publishing
- add release scripts and contributor documentation for versioning,
validation, and publishing
## Test plan
- Run `cargo check --locked --workspace` from `compiler/`.
- Run **(Compiler) Publish Rust Crates** from the Actions tab with **Dry
run** enabled; confirm every workspace crate packages successfully
without publishing.
- Run **(Compiler) Update Rust Crate Version** with a test version;
confirm it verifies one shared version and opens a signed version-bump
pull request containing the updated workspace manifest and lockfile.
Listeners registered with `options.signal` are supposed to be removed
when the signal is aborted.
Since `FragmentInstance` does not clean up its tracked listeners on
abort, previously removed listeners cannot be re-attached.
Fragment IntersectionObserver targets used to stay observed after a
child was removed so the exit record (isIntersecting: false) could still
fire, but that left the observer holding detached nodes. We now
unobserve ResizeObserver targets immediately, and delay
IntersectionObserver unobserve until after paint so the exit still
lands, then drop the strong ref.
If the same node is reinserted before that flush, we cancel the pending
unobserve so a later cleanup does not detach a child that’s still
visible.
Closes https://github.com/react/react/pull/37452/
Closes https://github.com/react/react/pull/37302
Fixes https://github.com/react/react/issues/37451
Bumps Jest to latest 30.x
The `resolutions` pin that kept jsdom at 22.1.0 is removed, so the test
environment now runs the jsdom version that jest-environment-jsdom
declares (26.1.0 on Jest 30).
The matcher aliases that Jest 30 deleted are replaced with their
canonical forms across the test suites: `toBeCalled`, `toBeCalledTimes`,
`toBeCalledWith`, and `lastCalledWith` become the corresponding
`toHaveBeenCalled*` matchers, and `toThrowError` becomes `toThrow`. The
custom `toThrow` matcher override is removed in the PR below this one.
Jest 30 activates the `node` export condition for CommonJS requires in
every test environment, so in the jsdom-based Flight suites
`react-server-dom-webpack/client` now resolves to the Node build (which
requires an options argument) instead of the browser build. Those suites
now map the client entry to `client.browser` explicitly, matching the
existing mocks for the `server` and `static` entries, and the Turbopack
Node test uses `jest.requireActual` because `client` and `client.node`
now resolve to the same file, which otherwise made the mock factory
recurse. For the same reason `react-dom/static` resolves to the
lazily-initialized Node entry in source mode, so its version-mismatch
test is gated to build mode like the other server-entry tests.
The obsolete `prettierPath` override is dropped from the base Jest
config because Jest 30 works with Prettier 3 for inline snapshots, which
also fixes `yarn test -u` crashing on the repo's Prettier 3-only hermes
plugin, and the now unused `prettier-2` alias dependency is removed with
it. Snapshot files are regenerated for Jest 30's updated snapshot header
and formatting. One test now passes a number instead of a string to
`jest.advanceTimersByTime`, which fake-timers v13 no longer coerces.
---------
Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>