We used to avoid updating .defaultValue until blur on number inputs
because of a Chrome bug, but the relevant Chromium behavior was fixed in
2020 in M85 https://issues.chromium.org/issues/40124871 and likewise in
WebKit https://bugs.webkit.org/show_bug.cgi?id=217156 so I think we can
probably just remove this special-cased logic.
We do still need some special logic for number inputs because it's the
only type where we let users specify a non-string value that has
multiple string representations and need to be careful not to clobber
the .value if it has a slightly different string than what you'd get
from casting a number to string.
Fixes#29862.
Tested with the number inputs fixture in current Chrome, Safari, and
Firefox.
Two bugs:
- Type changes to a lazy's inner component (e.g., adding a `memo`
comparison function) were missed by the refresh scan.
- If a SimpleMemoComponent needs to be remounted, previously
resolveRemountTypeForHotReloading would return the inner function
and we would incorrectly recreate the fiber as a FunctionComponent.
Instead, we can just use elementType consistently now that the
beginWork functions consistently call resolveFamily on the wrapped
components.
Editing the second argument of memo() previously never took effect
until something else remounted the tree, for two separate reasons:
- When adding a comparison function, we need to switch from
SimpleMemoComponent to MemoComponent so canPreserveStateBetween
should return false.
- MemoComponent was missing from the hot reload type resolution in
createWorkInProgress, so existing fibers kept reading .compare from
the old memo object forever.
New behavior:
- Adding or removing the comparison function remounts; we need to do
this when adding (SimpleMemoComponent doesn't support a comparison
function) so let's also do it when removing.
- Editing the comparison function implementation applies in place
with state preserved
The TODO in isCompatibleFamilyForHotReloading is removed as that
wasn't the right place to do this check.
If an edit changed the kind of the type (e.g. a plain function got
wrapped in memo()), the fiber could get the old kind's tag with the
new kind's type, usually throwing an error.
Previously resolveForwardRefForHotReloading was treated as a special
case. It's simpler to move this logic to updateForwardRef which
mirrors how updateMemoComponent and mountLazyComponent already are,
and it allows us to unify to a single resolveTypeForHotReloading which
we'll use in the next commit.
Since .type is no longer changing we need to set didReceiveUpdate
manually.
This behavior is already tested in "can update forwardRef render
function in isolation".
Same as #35227, but for `set`.
V8's `stack` setter silently noops when `this` is a Proxy
(nodejs/node#60862), so prodstack-stripping assignments like
`error.stack = 'Error: ' + message` in `resolveErrorProd` were dropped
and reads returned the real stack.
This caused `yarn test --prod` failures on Node 21+.
## Test Plan
`yarn test --prod` passes on both old and new Nodes.
Co-authored-by: Hanzo Dev <[email protected]>
Claude found this while running the Flight/Fizz bench.
---
While measuring memory in this fixture, the Flight server appeared to
leak ~140KB per render: `heapUsed` grew steadily across hundreds of
renders even with forced GC, and roughly 3x worse when a Flight client
consumed the stream in the same process. Fizz-only runs were flat, so it
looked like a real Flight leak.
On a closer look, the benchmark loop seems to be lying. React schedules
one `setImmediate` per request, but every render in this fixture
completes entirely in promises and `nextTick` callbacks, so a tight
benchmark loop never gives the event loop a chance to run immediates at
all. They quietly pile up, and each pending callback keeps its
already-finished request alive.
Heap snapshots show that every "leaked" request was held by a pending
immediate, and nothing else. Adding a single `await setImmediate` per
iteration makes memory completely flat over 300 renders, with or without
a same-process Flight client. A real server yields to the event loop on
every request, so this can't happen outside a synthetic loop.
The fix is to yield to the event loop between benchmark iterations,
outside the timed window, so latency numbers are unaffected. Each
variant now also reports how much heap it retains after the run settles.
That number was meaningless before — the pile-up made variants with
bigger per-request graphs look like they used more memory when they
didn't.
After the fix, a full `yarn bench:bare` run retains +0.3–0.6MB total per
variant after 1000 iterations (JIT/warmup noise), instead of drifting by
hundreds of MB.
---------
Co-authored-by: Hanzo Dev <[email protected]>
## Summary
Newer V8 versions (Node.js ≥ ~22) name the stack frames of static
methods on the `Promise` constructor `Promise.all`, `Promise.race`,
etc., where older versions named them `Function.all`, `Function.race`,
etc. The `isPromiseCreationInternal` and `isPromiseAwaitInternal`
allowlists in `ReactFlightServer` only matched the old spellings.
## How did you test this change?
- `yarn test --no-watchman ReactFlightAsyncDebugInfo` — 18/18 pass on
both Node 20.19.0 and Node 24.16.0 (previously, "can track async
information when awaited" failed on Node 24)
- `yarn test --silent --no-watchman packages/react-server/src/__tests__
packages/react-server-dom-webpack` — 243/243 pass on both Node versions,
default and `-r=experimental` channels
- `yarn test-www --silent --no-watchman
packages/react-server/src/__tests__` with `__VARIANT__` true and false —
pass
- `yarn flow dom-node`, `yarn prettier`, `yarn linc` — pass
Co-authored-by: Hanzo Dev <[email protected]>
## Summary
The compiler silently rewrote functions containing `using` and `await
using` declarations (explicit resource management). BuildHIR's
`VariableDeclaration` case only special-cases `var`; every other kind
falls through to `InstructionKind.Const`. A `using` declaration
therefore compiled as a plain memoized `const`, and the implicit dispose
call at scope exit was dropped from the compiled output. No diagnostic
was emitted. The Rust port mirrored the same bug in `build_hir.rs`.
The Rust port had a second, harder failure: its
`VariableDeclarationKind` enum had no `await using` variant, so a Babel
AST containing one failed serde deserialization at the NAPI boundary
("unknown variant `await using`") and threw for the whole file.
The fix records the same per-function Todo bail as the existing `var`
case, in both implementations, then continues lowering the declaration
as `const` so references do not break while the error unwinds (mirroring
how `var` continues as `let`). This matches the compiler's established
Todo-bail precedent for unsupported syntax: the function is skipped with
a logged `CompileError` diagnostic, `panicThreshold` is respected, and
sibling functions in the same file still compile. This deliberately
contrasts with oxc-project/oxc#24217, which handles the same syntax with
a silent per-function skip; nothing in this compiler is skipped without
a surfaced diagnostic.
Changes:
- TS: `BuildHIR.ts` records a Todo `CompilerErrorDetail` for `using` and
`await using` kinds, in the style of the existing `var` case.
- Rust: same Todo `record_error` in `build_hir.rs`. New `AwaitUsing`
variant (serde name `"await using"`) in `react_compiler_ast`, so `await
using` survives the NAPI boundary, covered by a serde round-trip unit
test.
- Test harness: enables the `explicitResourceManagement` parser plugin
in snap and in the e2e script's Babel baseline so fixtures can use the
syntax.
Fixtures (identical snapshots for the TS and Rust pipelines):
- `error.todo-using-declaration` and
`error.todo-await-using-declaration` show the Todo diagnostic with code
frames.
- `using-declaration-bailout-sibling-compiles` shows the bailing
function left untouched (disposal preserved) while a sibling component
in the same file is memoized.
The first commit snapshots the previous broken behavior (`using`
memoized as `const` with disposal dropped); the second commit lands the
fix and updates the snapshots.
## How did you test this change?
All results below are from the branch rebased onto current main.
TS implementation:
- `yarn snap`: 1809 tests, 1809 passed, 0 failed.
- `yarn workspace babel-plugin-react-compiler lint`: clean. `yarn
prettier-check` at the repo root: clean.
- Manually verified pre-fix output: a component with `using resource =
getResource(props.id)` compiled to a memoized `const` with no disposal.
Rust implementation:
- `cargo test --workspace`: 42 passed, 0 failed (includes the new serde
round-trip unit test for both `using` kinds).
- `bash scripts/test-babel-ast.sh`: passed.
- `bash scripts/test-rust-port.sh`: 1808 passed, 0 failed.
- `yarn snap --rust`: 1809 tests, 1809 passed, 0 failed, against the
same `.expect.md` snapshots as the TS run.
- `scripts/test-e2e.ts` (babel variant): the three using fixtures pass
code and events parity.
- Manually verified `await using` now deserializes across the NAPI
boundary and records the Todo instead of throwing.
<div><a
href="https://cursor.com/agents/bc-5fe4350d-5d42-4c0e-9001-429ed0b5e5b7"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a
href="https://cursor.com/background-agent?bcId=bc-5fe4350d-5d42-4c0e-9001-429ed0b5e5b7"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
---------
Co-authored-by: Hanzo Dev <[email protected]>
Fixes#30659. I'm not confident in this yet but here's what Fable said
based on
https://github.com/react/react/pull/32214#issuecomment-2734436781:
Editing a component from a plain function to a memo or forwardRef
wrapper (or between wrapper kinds) crashed with "Component is not a
function", because canPreserveStateBetween only compared hook
signatures. The edit was classified as state-preserving, so
createWorkInProgress swapped the wrapper object in as the type of a
FunctionComponent fiber and renderWithHooks tried to call it.
Two fixes, both DEV-only:
- canPreserveStateBetween returns false when typeof or $$typeof differ.
A fiber's tag is derived from the kind of its type, so state can never
be preserved across a kind change; these edits must go to staleFamilies
(remount). Nested wrappers need no special handling because register()
creates a family per nesting level ($type/$render ids), and each level's
kind is compared against its own family.
- The reconciler now finds and remounts wrapper fibers whose outer kind
changed. scheduleFibersWithFamiliesRecursively previously resolved
families only through the inner implementation (type/type.render), but a
kind-changing edit is recorded only on the outer type's family, so edits
like memo -> function never reached the scanner and were silently
dropped. The scanner now also checks the elementType's family, and the
_debugNeedsRemount branch in beginWork rebuilds the fiber from
family.current (the latest registered type for the fiber's identity)
rather than the fiber's possibly-stale inner type. This also fixes stale
simple memo remounts recreating as plain functions, dropping the memo
wrapper.
Unlike the reverted #30660 (see #32214), this does not fabricate
families or guess type shapes: families are still created in exactly one
place, and the same ID always resolves to the same family.
Restores the tests from #30660 and adds coverage for changing the inner
type of a memo between function and forwardRef.
Co-authored-by: BIKI DAS <[email protected]>
Co-authored-by: dan <[email protected]>
## Summary
Fixes#36601
### Problem
In `lowerJsxElementName` (BuildHIR.ts), the condition `if
(tag.match(/^[A-Z]/))` only treats JSX tags starting with an **uppercase
letter** as component references. Tags starting with `_`, `$`, or any
other non-letter character fell through to the `else` branch and were
incorrectly classified as `BuiltinTag` (host/intrinsic elements).
This meant that `<_Bar />` or `<$Foo />` was treated like `<div />`,
causing the compiler to skip memoization of the component and
potentially producing incorrect output.
### Root Cause
JSX semantics (as implemented by Babel's JSX transform) are:
- Tag starts with **lowercase** letter → host/intrinsic element (string
tag)
- **Everything else** → component reference (in-scope identifier)
The original code only handled the first half of that rule ("starts with
uppercase → component") while ignoring identifiers like `_Bar` and
`$Foo`.
### Fix
Change:
```ts
if (tag.match(/^[A-Z]/)) {
```
To:
```ts
if (!tag.match(/^[a-z]/)) {
```
This correctly classifies any JSX identifier that does NOT start with a
lowercase letter as a component reference, matching JSX spec semantics.
### Test
Added fixture `jsx-underscore-prefix-component` that renders `<_Bar />`
and verifies the compiler correctly memoizes it as a component
reference.
## How did you test this change?
- Added new compiler fixture test: `jsx-underscore-prefix-component`
- Ran `yarn workspace babel-plugin-react-compiler lint` ✅
- Ran snap tests with the new fixture to generate expected output ✅
## Summary
#34493 added `@tanstack/react-virtual`'s `useVirtualizer` to the known
incompatible libraries, but not `useWindowVirtualizer`.
Both hooks are thin wrappers around the same internal
`useVirtualizerBase`, so they return the same referentially-stable
virtualizer instance. Its methods (e.g. `getVirtualItems()`,
`getTotalSize()`) return internally-mutated values rather than new ones,
which is exactly the "interior mutability" pattern that breaks
memoization described in the module type provider.
Because only `useVirtualizer` was registered, code using
`useWindowVirtualizer` is silently compiled with the same unsafe
memoization. In our app this froze `getVirtualItems()` to its
first-render value (an empty `[]` before measurement), producing
permanently-empty virtualized lists — the same class of bug #34493
fixed, just via the window variant.
This registers `useWindowVirtualizer` alongside `useVirtualizer` with
the identical shape, in **both** implementations:
-
`compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts`
-
`compiler/crates/react_compiler_hir/src/default_module_type_provider.rs`
(Rust port)
See also the community report referenced in #34493:
https://github.com/TanStack/virtual/issues/736#issuecomment-3065658277
## How did you test this change?
Each addition mirrors the existing `useVirtualizer` entry exactly (same
kind, params, return type, and `knownIncompatible` message shape); both
are data-only additions to the module type providers, following the
precedent set by #34493 which changed only the TS file. The Rust change
keeps the two providers in sync.
## Summary
In a large react app, especially when components having similar starting
names like Table, TableColumn, TableCell, TableRow all together 100+
components when rendered in a virtualized table. Traversing the search
result is sometimes difficult with scroll
The component tree search only let you step through matches one at a
time (Enter / Shift+Enter). In large apps with many similarly-named
components (Table, TableRow, TableCell, ...) a search can return 100+
matches in a virtualized list, making a specific match tedious to reach.
- the result counter is an editable, live-scrubbing
index field: typing a number scrolls to that match as you type
(clamped to range)
- Fixes re-search getting stuck, clearing the box and retyping the same
term while a match was still selected snapped back to that same
component. It now advances to the next match (find-next semantics).
## How did you test this change?
Adds a SearchableTable example to the DevTools shell and unit tests for
the new action and the retype behavior.
https://github.com/user-attachments/assets/7ea9801a-7bcb-4e8f-bf73-a5307a0fdbae
cc @hoxyq Let me know what do you feel about this feature, if its
helpful for devtools.
`printOperationsArray` in `react-devtools-shared` walks an operations
array under
the invariant that each `switch` case leaves `i` pointing at the next
opcode (loop
header at `packages/react-devtools-shared/src/utils.js`).
The `TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE` case broke that
invariant:
```js
case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
i++; // skip opcode -> i now at the value slot
const activitySliceIDChange = operations[i + 1]; // reads the slot AFTER the value; i not advanced
...
}
```
The operation is exactly two slots, `[opcode, activitySliceID]` (see the
writer in
`packages/react-devtools-shared/src/backend/fiber/renderer.js`, which
pushes the
opcode then the id). So the case did two things wrong:
1. It logged the wrong number: `operations[i + 1]` reads the slot
*after* the
value (the next operation's opcode, or `undefined` at the end of the
array).
2. It left `i` pointing at the value slot, so the outer `while (i <
operations.length)` loop re-read the activity-slice id as an opcode. For
any
non-zero slice id that falls through to `default: throw
Error("Unsupported
Bridge operation ...")`, aborting the whole dump.
The two canonical decoders of this same operation both use the correct
pattern
(skip the opcode, then read *and* advance past the value):
- `devtools/store.js`: `i++; nextActivitySliceID = operations[i++];`
- `devtools/views/Profiler/CommitTreeBuilder.js`: `i++; const
activitySliceIDChange = operations[i++];`
This change makes `printOperationsArray` match them by reading
`operations[i++]`.
This is a debug-only diagnostic path: the only caller is the
`__DEBUG__`-guarded
dump in `backend/legacy/renderer.js`, so it is not a production crash.
The bug was
introduced in #34908.
## How did you test this change?
Added a regression test for `printOperationsArray` in
`packages/react-devtools-shared/src/__tests__/utils-test.js`. The
fixture chains
two activity-slice operations, `[rendererID, rootID, stringTableSize=0,
opcode, 42,
opcode, 0]`; the trailing operation is what forces the reader to advance
past the
first value slot rather than re-read it. It asserts the call does not
throw, logs
once, and that the message contains both `Applied activity slice change
to 42` and
`Reset applied activity slice`.
Ran the DevTools Jest project (built first, as that project requires a
build):
- With the fix: 51/51 pass, including the new test.
- Reverting only the one-line fix back to `operations[i + 1]` and
rebuilding: the
new test fails with `Unsupported Bridge operation "42"` (exactly the
predicted
failure), 50 pass / 1 fail. Restored the fix and it is green again.
`yarn prettier-check` and `yarn linc` are clean on the changed files.
`validateChildKeys` in `packages/react/src/jsx/ReactJSXElement.js` had a
signature
of `validateChildKeys(node, parentType)`, but #34174 ("Remove unused
arguments from
ReactElement") dropped the second argument, changing the signature to
`validateChildKeys(node)` and updating every call site to pass a single
argument.
That change removed several now-unused `@param` lines from the same
file, but left
one behind on `validateChildKeys`:
```js
/**
* ...
* @internal
* @param {ReactNode} node Statically passed child of any type.
* @param {*} parentType node's parent's type. // <- no such parameter anymore
*/
function validateChildKeys(node) {
```
`parentType` no longer appears anywhere in the function signature or
body, so this
`@param` line is stale and misleading to anyone reading the doc comment.
This PR
deletes that single line. The remaining `@param {ReactNode} node`
already fully and
correctly documents the sole parameter. No code or behavior change.
## How did you test this change?
This is a documentation-only change (a JSDoc comment on an `@internal`
helper), so
there is no runtime behavior to test. I verified it as follows:
- Confirmed `parentType` no longer appears anywhere in
`packages/react/src/jsx/ReactJSXElement.js` (`grep -n parentType`
returns no
matches after the change).
- Confirmed the signature `function validateChildKeys(node)` and all
call sites are
unchanged by this diff.
- `yarn prettier` (via `scripts/prettier/index.js check-changed`) -
clean.
- `yarn linc` (ESLint on changed files) - passed.
- `yarn flow dom-node` - No errors.
---
## Diff (for reference)
```diff
diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js
@@ -860,7 +860,6 @@ export function cloneElement(element, config, children) {
*
* @internal
* @param {ReactNode} node Statically passed child of any type.
- * @param {*} parentType node's parent's type.
*/
function validateChildKeys(node) {
```
`formatConsoleArguments` in
`packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js`
is used by the DevTools backend (via `hook.js`) to inline `console.*`
printf-style substitutions after stripping React's appended component
stack.
For `%s`/`%d`/`%i`/`%f` it consumes the next argument with
`args.splice(argumentsPointer, 1)` and formats the result.
When a format string has more specifiers than arguments, `splice`
returns an
empty array, so `arg` is `undefined` and the specifier is rendered as
text:
`%s` becomes `"undefined"`, and `%d`/`%i`/`%f` become `"NaN"`.
```js
formatConsoleArguments('%s %s', 'the');
// before: ['the undefined']
// after: ['the %s']
```
Browsers and Node's `util.format` leave an unmatched specifier as a
literal
(`console.log('%s %s', 'a')` prints `a %s`; `console.log('%d')` prints
`%d`).
So a message like `console.warn('value: %s')` was shown in DevTools as
`value: undefined` instead of `value: %s`.
This guards each of the `%d`/`%i`, `%f`, and `%s` cases on argument
availability (`argumentsPointer >= args.length`): when nothing is left
to
consume it keeps the specifier text and does not splice, mirroring the
existing trailing-`%` handling added in #36852. An explicitly passed
`undefined`/`null` argument is unchanged and still renders as
`undefined`/`null`, since a value is present at that position (the
`formats nullish values` test still passes).
## How did you test this change?
Added a regression test to the existing `formatConsoleArguments`
describe
block in `packages/react-devtools-shared/src/__tests__/utils-test.js`:
```js
it('keeps specifiers literal when no argument is supplied', () => {
expect(formatConsoleArguments('%s %s', 'the')).toEqual(['the %s']);
expect(formatConsoleArguments('%s %d', 'value')).toEqual(['value %d']);
expect(formatConsoleArguments('%s %i', 'value')).toEqual(['value %i']);
expect(formatConsoleArguments('%s %f', 'value')).toEqual(['value %f']);
});
```
Each assertion fails on `main` (it produces `['the undefined']` and
`['value NaN']`) and passes with the fix.
Commands run locally:
```
yarn test --build --project devtools packages/react-devtools-shared/src/__tests__/utils-test.js
# 51 passed, 51 total
yarn lint packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js \
packages/react-devtools-shared/src/__tests__/utils-test.js
# Lint passed.
yarn flow dom-node
# No errors!
yarn prettier-check packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js \
packages/react-devtools-shared/src/__tests__/utils-test.js
# clean
```
Cross-checked the expected output against Node `util.format`:
`util.format('%s %s', 'the')` -> `the %s`; `util.format('%d')` -> `%d`.
`formatConsoleArgumentsToSingleString` in
`packages/react-devtools-shared/src/backend/utils/index.js`
inlines `console.*` printf-style substitutions into a single string.
That string is
used both as the dedup key and as the displayed text for per-component
warnings/errors.
The `switch` that consumes the captured flag handles `s`, `d`, `i`, and
`f`, and the
function's own header comment says it "Implements s, d, i and f
placeholders". But
the substitution regex only captured `[jds]`:
```js
const REGEXP = /(%?)(%([jds]))/g;
```
So `%i` and `%f` were never matched. The `case 'i'` and `case 'f'` arms
were dead
code: the specifier was emitted literally and its argument was never
consumed. Worse,
because the unmatched specifier does not shift its argument, every
following specifier
in the same format string then binds to the wrong argument (a cascading
off-by-one
over the remaining args).
`%i` and `%f` are standard console integer/float specifiers (Node
`util.format` and
browsers both support them), so this affected common log formats. The
fix adds `i`
and `f` to the regex class so the existing switch arms run:
```js
const REGEXP = /(%?)(%([jdisf]))/g;
```
This is a one-character-class change that reconciles the regex with the
switch and
the header comment. The pre-existing behavior that `%j` is matched but
has no
`case` (so it falls through unchanged) is intentionally left as-is; it
is out of
scope for this fix.
## How did you test this change?
Added three regression tests to the existing
`formatConsoleArgumentsToSingleString`
describe block in
`packages/react-devtools-shared/src/__tests__/utils-test.js`:
- `formatConsoleArgumentsToSingleString('%i', 3.14)` -> `'3'`
- `formatConsoleArgumentsToSingleString('%f', 3.5)` -> `'3.5'`
- `formatConsoleArgumentsToSingleString('a %i b %s', 7, 'x')` -> `'a 7 b
x'` (locks
in argument alignment)
Commands run locally (experimental devtools bundles):
```
yarn build-for-devtools
yarn test --build --project=devtools -r=experimental packages/react-devtools-shared/src/__tests__/utils-test.js
```
Result: `Tests: 53 passed, 53 total`.
To confirm the tests actually cover the bug, I reverted the
one-character fix back to
`[jds]` and reran: the three new tests fail exactly as the bug predicts,
e.g. `%i`
yields `"%i 3.14"` and `a %i b %s` yields `"a %i b 7 x"` (the `%s` binds
to `7`
instead of `x`, showing the off-by-one). Restoring the fix makes them
pass again.
Also green:
```
yarn linc # ESLint on changed files: passed
yarn prettier # no files reflagged
yarn flow dom-node # No errors!
```
This PR adds a new `getParentStack` tool to the Facade and an
integration of it in `react-devtools-cdt-mcp`.
Similarly to the owner stack tool, it receives a `uid` of a specific
component and returns structural parent stack. For every node, it
includes `uid`, `name`, `type`.
Follow-up to #36903
The previous change stopped `onAllReady` from firing after a fatal shell
error by assigning `noop` to `request.onAllReady` inside `fatalError`.
This mutated a completion callback on the request as a side channel,
mirroring the existing `request.onShellError = noop` in `completeShell`.
This refactor removes both noop assignments and instead guards the calls
at the point where they would happen, so the callbacks on the request
stay the ones the caller passed in.
For `onAllReady`, `erroredTask` now returns right after `fatalError` in
the root (`boundary === null`) case, so it no longer falls through to
the `allPendingTasks === 0` check that calls `completeAll`. This matches
the sibling `finishAbortedTask`, which already returns after a fatal
root error. Later task completions cannot reach `completeAll` either,
because `performWork` bails out once the request status is past `OPEN`.
For `onShellError`, `fatalError` now decides whether to call it based on
the same condition that governed the assignment: the shell is complete
exactly when `request.pendingRootTasks === 0`, and `completeShell` only
runs at that point, so guarding the call is equivalent to the previous
nulling out. `onFatalError` still always fires because the error is
always fatal to the request.
This is a behavior-preserving refactor of the fix landed in #36903; the
tests added there continue to pass unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
Two fundamental changes (plus a lot of formatting that got mixed in :/
): Modify how we determine if a local binding exists for the case where
the scope is extracted from a function, rather than a module, which is
the case for snap tests. And, loosen the rust port tester to allow debug
output to be printed from one compiler or another as long as both sides
error in the same phase with the same error message (basically, the rust
port emits debug information before throwing an error, whereas the TS
version does one or the other -- but it's not actually a real
difference).
With this change and loosening, the Rust compiler conforms on the
todo-locally-require-fbt case.
---------
Co-authored-by: mvitousek <[email protected]>
An alternative to https://github.com/facebook/react/pull/36135
___
Adds experimental support for `parentEnter` and `parentExit` on nested
`<ViewTransition>` components, so descendants can animate when an
ancestor boundary enters or exits.
This is gated behind the `enableViewTransitionParentEnterExit` feature
flag.
Addresses the use case of animating individual items in a list (such as
sliding inactive feed posts off-screen) when a parent `<ViewTransition>`
is the one being added or removed during the update.
Today, `exit`/ `enter` only fire on the `<ViewTransition>` that was
directly mounted or unmounted. Nested boundaries in the same subtree are
not visited for their own `exit`/`enter` when the whole tree goes away
as one unit, which is usually what you want. But some cases need an
explicit opt-in to nested animations.
This PR adds `parentEnter`/`parentExit`, and their handlers
`onParentEnter`/`onParentExit`. When an ancestor runs a real `enter` or
`exit`, React walks descendants and activates boundaries that define the
nested props. This only continues through `<ViewTransition>` nodes that
define `parentEnter` / `parentExit` / `onParentEnter` / `onParentExit` /
`onGestureParentEnter` / `onGestureParentExit`, otherwise the chaining
stops. A boundary’s own `enter`/`exit` still do not run when only an
ancestor animates. So it is possible to have different animations on the
same `<ViewTransition>` for `enter` vs `parentEnter`.
```jsx
function ExampleOnAncestorExit() {
return (
// This boundary exits — starts the parentExit walk
<ViewTransition exit="panel-exit">
<div>
{/* ✓ Activates: parentExit set. Walk continues inside */}
<ViewTransition parentExit="slide-out">
<div>
{/* ✗ parentEnter is not consulted on an exit walk */}
<ViewTransition parentEnter="slide-in">
...
</ViewTransition>
{/* ✗ Chain broken: no parentExit or onParentExit */}
<ViewTransition>
<div>
{/* ✗ Never reached on this path, blocked by parent above */}
<ViewTransition parentExit="slide-out">
...
</ViewTransition>
</div>
</ViewTransition>
{/* ✓ Handler-only chain: onParentExit continues the walk */}
<ViewTransition onParentExit={() => animateOut()}>
<ViewTransition onParentExit={() => animateDeepOut()}>
...
</ViewTransition>
</ViewTransition>
{/* ✓ Activates: sibling under the chain */}
<ViewTransition parentExit="slide-out">
...
</ViewTransition>
</div>
</ViewTransition>
</div>
</ViewTransition>
);
}
```
A node runner that bootstraps `fixtures/app`, and then uses
`chrome-devtools` CLI to test third-party tools.
```
$ node e2e/run.js
Starting fixture at http://127.0.0.1:60033/
Starting chrome-devtools daemon...
Checking third-party tool discovery...
Checking tree, details, search, and DOM lookup...
Checking source, owners, and error payloads...
Checking profiling through a real CLI click...
react-devtools-cdt-mcp E2E passed.
✨ Done in 11.66s.
```
In `recoverFromConcurrentError`, there's logic to determine if the
latest render attempt resulted in another error or if it completed
successfully, by checking whether the exit status matches RootErrored.
The logic was incomplete because RootSuspendedAtTheShell is also
considered an errored state in this context. This can cause an
incomplete tree (i.e. one that unwinds without entering the complete
phase) to be mistaken for a complete one, leading to confusing errors.
An incomplete tree can never be committed because it's not guaranteed to
represent a coherent state.
An example of how this can manifest as a bug: in #33580, an error caught
by an error boundary triggers a synchronous recovery render. During that
render a parent component suspends on `use(thenable)` with no Suspense
boundary above it, so the tree unwinds to the shell instead of
completing. Because the incomplete tree is mistaken for a recovered one
and committed, the parent fiber becomes current with a truncated hook
list — only the hooks it rendered before suspending. On the next render
it calls the rest of its hooks and throws "Rendered more hooks than
during the previous render."
Fixes#33580.
Co-authored-by: Arunanshu Biswas
<[email protected]>
Co-authored-by: Arunanshu Biswas <[email protected]>
Integrates newly added `getComponentByHostInstance` tool from
`react-devtools-facade` into `react-devtools-cdt-mcp`.
The API uses `uid` of the element from CDT MCP tree snapshot, but under
the hood it gets converted to the DOM Element reference, which is
exactly what we pass to `getComponentByHostInstance`.
Adds a new tool to the Facade, where the input is the `HostInstance` and
the output is the corresponding component payload for the `HostFiber` of
this `HostInstance`. Supports multiple Host objects from Fiber:
`HostComponent`, `HostText`, `HostSingleton`, `HostHoistable`.
> [!NOTE]
> This doesn't bring much value now, because this returns the payload
for the corresponding HostFiber, not the closest user-defined Fiber.
This will be improved by adding a parentUid reference to the element
payload, so the agent can crawl up through the component tree.
> [!NOTE]
> I had to invalidate `node_modules` cache keys on CI, because we are
now using `react-devtools-facade` package and we need to create a
symlink to it, otherwise Flow will fail. We use a hash value of
`yarn.lock`, but it's not invalidated when a package inside yarn
workspace is using another private package.
---
This change introduces new `react-devtools-cdt-mcp` package, which is
responsible for installing React tools for `chrome-devtools-mcp` server.
It is using `react-devtools-facade` as a dependency: it doesn't own the
implementation of these tools, it just relays Facade's tools to
`chrome-devtools-mcp` format.
When CDT MCP navigates to a fresh page, it emits unique
`devtoolsdiscovery` method, that has `respondWith` method, which is the
integration point between runtime libraries and MCP server.
`react-devtools-cdt-mcp` is the owner for this integration. More details
are available here -
https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/docs/third-party-developer-tools.md.
Here is the list of tools that are being installed:
- `react_get_component_tree`
- `react_get_component_by_uid`
- `react_find_components`
- `react_get_component_source`
- `react_get_owner_stack_trace`
- `react_get_owner_stack`
- `react_start_profiling`
- `react_stop_profiling `
- `react_get_trace_overview`
- `react_get_commit_report`
---
Quick demo how to test the package with `chrome-devtools-mcp` CLI. Steps
are also mentioned in `README` file:
https://github.com/user-attachments/assets/af9fefce-03f0-4ebd-a94f-e9dd72b87f4d
## Summary
The Rust port (#36173) changed `CompileError` `LoggerEvent`s to carry
plain serialized detail objects instead of
`CompilerError`/`CompilerDiagnostic` class instances. As a result, the
ESLint integrations could no longer call
`detail.printErrorMessage(source, {eslint: true})` and were given a
replacement `printErrorMessage()` helper that only emitted the `reason`
and `description`.
This regressed error printing: the **source code frame(s) and
`file:line:column` location** that used to appear for each error detail
were dropped from lint output.
This PR restores the previous behaviour:
- Export `printCodeFrame` from `CompilerError` and reuse it from both
ESLint integrations instead of duplicating it.
- Rebuild the full message (reason, description, per-detail code frames,
and hints) in `printErrorMessage`.
- Handle **both** detail shapes that flow through `LoggerEvent`s:
- a `details` array (`CompilerDiagnostic` and the **Rust** compiler),
and
- a legacy flat `loc` (deprecated `CompilerErrorDetail`).
`formatDetailForLogging` emits one or the other, so the previous
unconditional iteration over `error.details` would have thrown
`TypeError: not iterable` on the flat-`loc` path. Normalizing to a list
fixes that and keeps the code working with both the TypeScript and Rust
compilers.
## Test plan
- `tsc --noEmit` on both ESLint packages is clean (no new errors vs.
baseline).
- Verified the detail loop handles the Rust compiler's `details` array
shape and the legacy flat `loc` shape.
The tool now has a boolean argument which controls the presence of hook
tree in the return payload. If hooks were requested, and the inspection
fails, the error payload will be returned instead.
Documents the patch releases that shipped across the 19.0, 19.1, and
19.2 lines (`.2`/`.3` through `.7`/`.8`), which were missing from the
changelog.
---------
Co-authored-by: Hanzo Dev <[email protected]>
Adds a test documenting why a `string.length`
fast path is unsound.
`byteSize` feeds heuristic decisions (outlining threshold at 500 bytes,
progressive chunk sizing). `string.length` undercounts multi-byte
content, so a boundary that is really >500 bytes could drop below the
outlining threshold.
---------
Co-authored-by: Hanzo Dev <[email protected]>
## Summary
The `runtime_commit_artifacts` workflow has been failing in the "move
relevant files for react in fbsource into compiled-rn" step with:
```
mv: cannot stat 'build/facebook-react-native/VERSION_NATIVE_FB': No such file or directory
```
`VERSION_NATIVE_FB` was only written in `processExperimental`, *inside*
`build/facebook-react-native/`. That directory is not part of the
allowlist in the cleanup loop at the end of `processExperimental`, so
the experimental workers wrote the version file and then immediately
deleted it.
This was masked until #36859. The cleanup loop previously used `rm -rm`
(an invalid flag), so `rm` errored out and deleted nothing — the loop
was effectively a no-op. Once it was changed to `fs.rmSync`, the
deletion actually took effect. After CI merges the per-worker artifacts,
`facebook-react-native/*/cjs` still survives (the **stable** workers
produce it and `processStable` has no cleanup loop), but
`VERSION_NATIVE_FB` is gone because stable never wrote it.
## Fix
The `build/facebook-react-native` artifacts are identical across release
channels:
- RN packages resolve to the generic `.fb.js` entry fork (no
`.modern.fb.js` / `.classic.fb.js` split).
- The `native-fb` feature-flag forks don't reference `__EXPERIMENTAL__`.
- The version string is identical
(`<ReactVersion>-native-fb-<sha>-<date>`).
So this writes `VERSION_NATIVE_FB` from `processStable`, where
`facebook-react-native` is preserved, and drops the now-dead write from
`processExperimental`.
## Test Plan
Verified locally by building the `react` package through each code path:
- **Stable channel** (`yarn build --r=stable react/index`):
`build/facebook-react-native/VERSION_NATIVE_FB` is written with the
correct `19.3.0-native-fb-<sha>-<date>` version string. ✅
- **Experimental channel** (`yarn build --r=experimental react/index`):
`build/facebook-react-native` is deleted by the cleanup loop, leaving
only `oss-experimental` and `facebook-www` — confirming the version file
cannot come from this channel. ✅
- **Local merged build** (`yarn build react/index`, builds both channels
and merges): the final `build/facebook-react-native/` contains
`VERSION_NATIVE_FB`, matching what CI produces after merging per-worker
artifacts. ✅
Instead of potential confusion coming from `stack` vs `branch`, we will
use `getOwnerStackTrace` for the error-like stack trace and
`getOwnerStack` for list of structured entities that represent owners.
A Node.js `Buffer` carries a `toJSON` method, so Flight serializes it
through that method instead of as binary, and it is deserialized as a
plain `{type: 'Buffer', data: [...]}` object rather than a `Buffer` or
`Uint8Array`. The warning React emits in this case reads "Uint8Array
objects are not supported", which is misleading on two counts: the value
is actually a `Buffer`, reported as `Uint8Array` only because that is
its `Object.prototype.toString` tag, and a real `Uint8Array` is in fact
supported, since it has no `toJSON` and serializes as binary. This makes
the message especially confusing when binary data such as a font is
passed as a serialized value.
The first commit adds a test that pins down the current behavior,
capturing both the misleading warning and the deserialization to a plain
`{type: 'Buffer', data: [...]}` object. The second commit replaces the
warning, for values that are `ArrayBuffer.isView`, with one that names
the actual cause and the fix: the data is serialized through `toJSON`
instead of as binary, so a `Uint8Array` or `ArrayBuffer` should be
passed to send binary data. The new branch only fires for a typed array
that also carries a `toJSON`, which in practice is a Node `Buffer`; a
plain `Uint8Array` or `ArrayBuffer` has no `toJSON`, never reaches this
branch, and continues to serialize as binary.
## Summary
- Render standalone DevTools server errors with DOM nodes instead of
HTML strings.
- Preserve the existing error box classes and copy while inserting error
text with `textContent`.
## How did you test this?
- `corepack yarn prettier`
- `corepack yarn lint packages/react-devtools-core/src/standalone.js`