Skip to content

Update the React DevTools integration to React DevTools 8.0.0 - #10677

Merged
Andarist merged 6 commits into
mainfrom
andarist/rdt-8.0.0
Oct 16, 2026
Merged

Andarist merged 6 commits into
mainfrom
andarist/rdt-8.0.0

Conversation

@Andarist

@Andarist Andarist commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Goes with the RDT routine update in replayio/backend#13510: the React DevTools operations now come from (Replay's fork of) React DevTools 8.0.0, which supports current React 19, and are read from the new react-devtools-bridge-v2 annotations.

  • Use upstream react-devtools-inline@8.0.0 (pinned exactly, see below) for the Store and bridges, instead of our 4.28 fork @replayio/react-devtools-inline and the yarn patch that made it work with React 19. Only the Store and the bridge were ever used from it, which upstream provides as is.

  • Read the operations from react-devtools-bridge-v2 annotations. The old react-devtools-bridge kind has the earlier operations format, which a parser can't tell apart from the new one (the bridge protocol version is 2 in both). With separate kinds, a frontend and backend that disagree (during a deploy or a rollback) show no React tree instead of a wrong one.

  • The bundle injected into pauses is now react-devtools-core's backend (hook, renderer and agent in one file), built from the same fork commit as the routine's (replayio/react@b01ae07908). The injection installs the hook with the default component filters and attaches an agent. It then injects the saved Fiber renderers in the order the routine does, so renderer IDs match, and commits the saved roots. That mounts the current tree under the same element IDs as the operations.

  • Page APIs are shadowed for the bundle like the routine does, so that nothing it does in a pause reaches the page:

    • Web Storage;
    • setTimeout/clearTimeout: the agent's timer that persists the selection;
    • console: the hook wraps console.error/console.warn;
    • reportError;
    • performance and matchMedia: resource timings and stylesheet media queries.

    queueMicrotask runs its callback right away, so the bridge, which nothing listens to here, sends each batch to nowhere instead of queueing messages forever.

  • __RECORD_REPLAY_REACT_DEVTOOLS_SEND_MESSAGE__ is now defined by the injection (it used to be part of the fork). It calls the agent's handler and returns the message it sends back, restoring the agent's bridge afterwards.

  • findNativeNodesForFiberID() is findHostInstancesForElementID() now.

  • Operations use the 7.0+ format. Resetting the displayed tree generates removals of everything the Store holds (Suspense nodes, then elements, children first), because React DevTools no longer has a "remove root" operation. It builds them from private Store fields, so react-devtools-inline has to stay pinned to exactly 8.0.0. This also fixes the subtree mode being read from the wrong index when printing operations.

Deploy after replayio/backend#13510. Until a recording has been processed by the new routine (version 14, which runs again the next time a recording is opened), there are no -v2 annotations, and the React panel is empty. The e2e React tests here fail until that backend change is deployed.

Goes with the RDT routine update in the backend: the operations in the
`react-devtools-bridge` annotations now come from (Replay's fork of)
React DevTools 8.0.0, which supports current React 19.

- Use upstream `react-devtools-inline@8.0.0` for the Store and bridges,
  instead of our 4.28 fork `@replayio/react-devtools-inline` and the yarn
  patch that made it work with React 19. Only the Store and the bridge
  were ever used from it, which upstream provides as is.
- The bundle injected into pauses is now react-devtools-core's backend
  (hook, renderer and agent in one file) from the same fork commit as the
  routine's. The injection installs the hook with the default component
  filters, attaches an agent, injects the saved Fiber renderers in the
  order the routine does (so renderer IDs match) and commits the saved
  roots, which mounts the current tree under the same element IDs as the
  operations. Web Storage and microtasks are shadowed for the bundle.
- `__RECORD_REPLAY_REACT_DEVTOOLS_SEND_MESSAGE__` is now defined by the
  injection (it used to be part of the fork), on the hook's agent.
- `findNativeNodesForFiberID()` is `findHostInstancesForElementID()` now.
- Operations use the 7.0+ format; the parser handles it, and resetting
  the displayed tree generates removals of everything the Store holds
  (Suspense nodes, then elements, children first), as React DevTools no
  longer has a "remove root" operation. This also fixes the subtree mode
  being read from the wrong index when printing operations.
@Andarist
Andarist requested a review from markerikson October 15, 2026 10:21
@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
devtools Ready Ready Preview Oct 16, 2026 1:51pm UTC

Request Review

@socket-security

socket-security Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedreact-devtools-inline@​8.0.010010010087100

View full report

…notations

The backend's RDT routine now saves the operations (in the format of
React DevTools 7.0+) under a new annotation kind. The previous kind
had the earlier format, which a parser can't tell apart (the bridge
protocol version is 2 in both), so with separate kinds a frontend and
backend that disagree, e.g. while one of them is being deployed or
rolled back, show no React tree instead of a wrong one.
The bridge batches outgoing messages and sends them from a microtask,
which the injection shadowed with a no-op: the first message scheduled a
flush that never ran, and every later one stayed queued. Nothing listens
to that bridge in a pause, so send each batch (to nowhere) right away.
@markerikson

Copy link
Copy Markdown
Collaborator

🤖review: Reviewed 1391370015. Moving to upstream react-devtools-inline@8.0.0 and dropping the yarn patch is a good cleanup. I checked that react_devtools_backend.raw.js is byte-identical to the backend's react_devtools_backend.js asset in replayio/backend#13510, and both headers cite fork commit b01ae07908. I haven't run the tests or tried it on a recording.

Comments, none blocking:

  1. Deploy after replayio/backend#13510. This reads only react-devtools-bridge-v2 annotations, so recordings without a v14 routine run will have an empty React panel. That backend PR also has a crash to fix first (performance shadowed as undefined).

  2. The in-pause injection shadows less than the backend does. rdtBackendWithGlobals shadows only localStorage, sessionStorage, queueMicrotask and module/exports. Two upstream side effects therefore run against the real page during the pause:

    • the agent's setTimeout(this._persistSelection, 1000) on selection;
    • the hook's console.error/console.warn patch.

    I don't see a correctness problem from either, but I'd rather remove them in the fork (see [DO NOT MERGE] Preview of Replay RDT fork react#18) than rely on shadowing here. If they stay, shadowing setTimeout/clearTimeout here would be cheap.

  3. __RECORD_REPLAY_REACT_DEVTOOLS_SEND_MESSAGE__ replaces agent._bridge with the capture object and never puts the original back. Nothing else uses that bridge, so it works. Restoring it in a finally would still be cheap.

  4. generateTreeResetOperations builds its ops from private Store fields (_idToElement, _idToSuspense, _rootIDToRendererID). That works, but it means react-devtools-inline should stay pinned to exactly 8.0.0 so an upgrade doesn't break it silently.

React DevTools reports exceptions thrown by its event listeners with
`reportError()`, which would dispatch an error event on the paused page,
and it reads resource timings (`performance.getEntriesByType`) and
evaluates stylesheets' media queries (`matchMedia`), neither of which
matters for the frontend. Shadow them like the backend's RDT routine
does.
…sole

The agent schedules a timer to persist the selection whenever an element is
inspected, and the hook wraps console.error/console.warn when it is
installed. Shadow setTimeout, clearTimeout and console for the bundle, like
the backend routine does. Also put the agent's bridge back after each
request instead of leaving the capture object in place.
…ols-02

React DevTools doesn't list the children of hidden Offscreen fibers. In the
logpoints-01 recording, which predates React's separate Activity fiber, a
hidden <Activity> is a plain hidden Offscreen, and one of the <Suspense>
components mounts inside one (in a hidden panel) and stays there until the
end of the recording. The old React DevTools fork listed it; 8.0 doesn't.
@Andarist
Andarist merged commit 4282d6e into main Oct 16, 2026
21 checks passed
@Andarist
Andarist deleted the andarist/rdt-8.0.0 branch October 16, 2026 18:11

This branch was successfully deployed

1 active deployment
Preview — 5664994a Deployed Oct 16, 2026 by vercel[bot]
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.

2 participants