Skip to content

feat: support React 19.3 - #3916

Open
cades wants to merge 2 commits into
pmndrs:masterfrom
cades:feat/react-19-3
Open

feat: support React 19.3#3916
cades wants to merge 2 commits into
pmndrs:masterfrom
cades:feat/react-19-3

Conversation

@cades

@cades cades commented Sep 10, 2026

Copy link
Copy Markdown

Fixes #3915.

What was wrong

The react / react-dom peer range was capped at <19.3, and that cap turned out to be load-bearing rather than precautionary.

React 19.3 added a types field to the transition object (19.2 mints {_updatedFibers}, 19.3 mints {types, _updatedFibers}). react-dom 19.3 reads it guarded against null, not against undefined:

newEventTime = transition.types;
if (null !== newEventTime) {
  for (newEventType = firstScheduledRoot; null !== newEventType; )
    queueTransitionTypes(newEventType, newEventTime), 

and queueTransitionTypes immediately reads transitionTypes.length.

The vendored reconciler is built from react-reconciler 0.33.0, which mints transitions with no types field. undefined passes the null !== … guard, and the read then throws.

The user-visible effect: any component inside the R3F tree calling useTransition() and updating state that lives in the surrounding DOM tree throws Cannot read properties of undefined (reading 'length'), surfaced through onUncaughtError — so it trips an error boundary rather than failing quietly.

The change

  • react-reconciler 0.33.0 → 0.34.0 (the release that declares react: ^19.3.0), which is what gets vendored by patch-react-reconciler
  • @types/react-reconciler 0.32.3 → 0.33.0 to match
  • scheduler ^0.27.0 → ^0.28.0, which is what react-reconciler 0.34.0 depends on — without this the package resolves two scheduler instances with independent task queues rather than one
  • peer react / react-dom >=19 <19.3>=19 <19.4, keeping the existing convention of an explicit upper bound
  • root dev react / react-dom ^19.2.0 → ^19.3.0, so local development and the test run exercise the React this targets

No source changes — the reconciler is a build artifact of patch-react-reconciler, so bumping the dependency is the whole fix. The one added file is a test (below).

Test

The suite had no coverage for the case this fixes — a transition started inside the R3F tree updating state in a react-dom root — which is why everything stayed green while that path threw. packages/fiber/tests/transitions.test.tsx covers that crossing.

I checked it actually catches the bug rather than assuming it does, by rebuilding the vendored reconciler both ways and running it against each:

vendored reconciler built from test
react-reconciler 0.33.0 fails
react-reconciler 0.34.0 passes

The setTimeout(50) in it is because the throw happens while the root is being scheduled rather than during the update, so the scheduled work has to run before the assertion. Happy to replace it if you have a preferred idiom for that in this suite.

Verification

Using the standalone reproduction from #3915, holding everything constant and swapping only the built dist:

dist React.startTransition (control) useTransition() inside the R3F tree
upstream 9.7.0 from npm no error Cannot read properties of undefined (reading 'length')
built from this branch no error no error
upstream again, with scheduler already at 0.28.0 no error throws again

The third row is there on purpose: it rules out the scheduler bump as the thing that fixed it, leaving the rebuilt reconciler as the cause. The control column staying clean throughout rules out the harness.

yarn test on React 19.3.0 — 16 suites, 174 passing, 1 todo, 14 snapshots, no snapshot changes. Before this branch: 15 suites, 173 passing; the difference is the added test.

Notes for review

  • I picked minor for the changeset, following 9.5.0 ("Support React 19.2"). Happy to change it to patch if you would rather treat it as a compatibility fix.
  • feat: support React 19.3 (v10) #3917 does the same thing on v10, which has the identical problem.
  • The upper bound is now <19.4, which keeps the current approach of pinning to a known-good React minor. If you would prefer to drop the upper bound, or to widen it differently, say so and I will adjust — I did not want to change that policy in a PR that is otherwise mechanical.
  • I have not tested the React Native / Expo paths; the change is not RN-specific, but I could not exercise them here.

🤖 Generated with Claude Code

The `react`/`react-dom` peer range was capped at `<19.3`, and the cap was
load-bearing: React 19.3 added a `types` field to the transition object, and
`react-dom` 19.3 reads it guarded against `null` rather than `undefined`. The
vendored reconciler was built from `react-reconciler` 0.33.0, which mints
transitions without that field, so `undefined` passed the guard and
`queueTransitionTypes` then threw on `undefined.length`.

In practice that meant any component inside the R3F tree calling
`useTransition()` and updating state in the surrounding DOM tree crashed with
`Cannot read properties of undefined (reading 'length')`, surfaced through
`onUncaughtError`. Reported in pmndrs#3915 with a standalone reproduction.

Rebuilding the vendored reconciler from `react-reconciler` 0.34.0 — the release
that declares `react: ^19.3.0` — fixes it. `scheduler` moves to ^0.28.0 to match
what 0.34.0 depends on, so the package keeps a single scheduler instance rather
than resolving two.

The peer range moves to `>=19 <19.4`, keeping the existing convention of an
explicit upper bound.

Verified against the reproduction from pmndrs#3915, holding everything else constant
and swapping only the built `dist`:

  upstream dist  -> useTransition inside the R3F tree throws
  this dist      -> no error
  upstream dist  -> throws again (with scheduler already at 0.28.0)

The third arm rules out the scheduler bump as the cause. React's own
`startTransition` is clean in every arm, which rules out the harness.

`yarn test` passes on React 19.3.0 with no snapshot changes: 15 suites, 173
passing, 1 todo, 14 snapshots — identical to the run before this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The suite had no coverage for the case the reconciler bump fixes: a component
inside the R3F tree starting a transition that updates state in a react-dom
root. Everything else stayed green while that path threw, which is why the
incompatibility reached a release.

Checked that it actually catches the bug rather than assuming it does, by
rebuilding the vendored reconciler both ways and running it against each:

  built from react-reconciler 0.33.0 -> fails
  built from react-reconciler 0.34.0 -> passes

The `setTimeout(50)` is because the throw happens while the root is being
scheduled rather than during the update, so the scheduled work has to run
before the assertion.

Suite on React 19.3.0: 16 suites, 174 passing, 1 todo, 14 snapshots unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

React 19.3: useTransition() inside the R3F tree throws "Cannot read properties of undefined (reading 'length')"

1 participant