From 75ef18a9172e4c100b5d5650ae14c395c5c8ef42 Mon Sep 17 00:00:00 2001 From: Shivam S <152616194+BIGSUS24@users.noreply.github.com> Date: Tue, 29 Sep 2026 21:09:28 +0530 Subject: [PATCH 1/4] Add ViewTransition and addTransitionType to the reference indexes (#8673) Both became stable in React 19.3 and both have reference pages and sidebar entries, but neither is listed on its overview page: - `` is missing from Built-in React Components, which already lists its 19.3 sibling ``. - `addTransitionType` is missing from Built-in React APIs, which already lists `cache`, `cacheSignal` and `captureOwnerStack`. CONTRIBUTING.md says the API Reference "is intended to be exhaustive", so a reader browsing either overview page currently can't discover either API. Co-authored-by: Claude Opus 5 (1M context) --- src/content/reference/react/apis.md | 1 + src/content/reference/react/components.md | 1 + 2 files changed, 2 insertions(+) diff --git a/src/content/reference/react/apis.md b/src/content/reference/react/apis.md index 6aa91ed0d..1e6a8a644 100644 --- a/src/content/reference/react/apis.md +++ b/src/content/reference/react/apis.md @@ -14,6 +14,7 @@ In addition to [Hooks](/reference/react/hooks) and [Components](/reference/react * [`lazy`](/reference/react/lazy) lets you defer loading a component's code until it's rendered for the first time. * [`memo`](/reference/react/memo) lets your component skip re-renders with same props. Used with [`useMemo`](/reference/react/useMemo) and [`useCallback`.](/reference/react/useCallback) * [`startTransition`](/reference/react/startTransition) lets you mark a state update as non-urgent. Similar to [`useTransition`.](/reference/react/useTransition) +* [`addTransitionType`](/reference/react/addTransitionType) lets you specify the cause of a Transition. Used with [`startTransition`](/reference/react/startTransition) and [``.](/reference/react/ViewTransition) * [`act`](/reference/react/act) lets you wrap renders and interactions in tests to ensure updates have processed before making assertions. * [`cache`](/reference/react/cache) lets you cache the result of a data fetch or computation. * [`cacheSignal`](/reference/react/cacheSignal) lets you know when the `cache()` lifetime is over. diff --git a/src/content/reference/react/components.md b/src/content/reference/react/components.md index 081f206c0..bd5a30112 100644 --- a/src/content/reference/react/components.md +++ b/src/content/reference/react/components.md @@ -17,6 +17,7 @@ React exposes a few built-in components that you can use in your JSX. * [``](/reference/react/Suspense) lets you display a fallback while the child components are loading. * [``](/reference/react/StrictMode) enables extra development-only checks that help you find bugs early. * [``](/reference/react/Activity) lets you hide and restore the UI and internal state of its children. +* [``](/reference/react/ViewTransition) lets you animate a component tree with Transitions and Suspense. --- From 8c68ae8d2410abe59f351195780c6f8ea9f50904 Mon Sep 17 00:00:00 2001 From: Soichiro Miki Date: Fri, 2 Oct 2026 01:33:59 +0900 Subject: [PATCH 2/4] Use relative link, without domain, to Compiler docs (#8678) --- src/content/reference/react-compiler/directives.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/reference/react-compiler/directives.md b/src/content/reference/react-compiler/directives.md index 705d0f620..df0f053e8 100644 --- a/src/content/reference/react-compiler/directives.md +++ b/src/content/reference/react-compiler/directives.md @@ -195,4 +195,4 @@ For specific issues with directives, see the troubleshooting sections in: * [`compilationMode`](/reference/react-compiler/compilationMode) - Configure how the compiler chooses what to optimize * [`Configuration`](/reference/react-compiler/configuration) - Full compiler configuration options -* [React Compiler documentation](https://react.dev/learn/react-compiler) - Getting started guide \ No newline at end of file +* [React Compiler documentation](/learn/react-compiler) - Getting started guide \ No newline at end of file From 046f17d04295ba047bb5739026b4ac3110f17028 Mon Sep 17 00:00:00 2001 From: Aurora Scharff <66901228+aurorascharff@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:07:58 +0200 Subject: [PATCH 3/4] Clarify image loading during View Transitions (#8671) * Clarify img View Transition example * Avoid redundant image alt text * Tighten img example introduction * Move img example outcome below demo * Clarify Suspense image example * Simplify View Transition comparison examples * Use a Suspense load in the image example * Align Suspense resource loading examples * Reduce View Transition example changes * Remove artificial image loading delay * Keep Suspense explanation self-contained * Scope shared examples to each API * Use Suspense for View Transition image example * Make image loading comparison visible * Tighten View Transition image examples * Document View Transition resource timeout * Cache image promises in Suspense examples * Fix Suspense resource demos * Clarify View Transition image reveal * Fix Suspense font loading example * Align image loading explanations * Address image documentation review * Simplify View Transition image guidance --- .../reference/react-dom/components/img.md | 88 +++++---- src/content/reference/react/Suspense.md | 182 ++++++++++-------- src/content/reference/react/ViewTransition.md | 17 +- 3 files changed, 155 insertions(+), 132 deletions(-) diff --git a/src/content/reference/react-dom/components/img.md b/src/content/reference/react-dom/components/img.md index 4a1f45077..255aee8d6 100644 --- a/src/content/reference/react-dom/components/img.md +++ b/src/content/reference/react-dom/components/img.md @@ -51,7 +51,7 @@ To display an image, render the [built-in browser `` component](https://dev * Do not pass an empty string to `src`. It may cause the browser to request the current page again. React warns in development and omits the attribute. To render no image, omit the `` or pass `null` to `src`. * `` cannot have children or use `dangerouslySetInnerHTML`. React throws an error if you pass either. -* `fetchPriority="low"` does not stop React from waiting for the image to load and decode during a client-rendered View Transition update. Use `loading="lazy"` or an `onLoad` handler to opt out of that behavior. +* `fetchPriority="low"` does not stop React from waiting for the image to load during a client-rendered View Transition update. Use `loading="lazy"` or an `onLoad` handler to opt out of that behavior. --- @@ -125,25 +125,26 @@ To create an explicit preload hint, call [`preload`](/reference/react-dom/preloa ### Waiting for an image during a View Transition {/*waiting-for-an-image-during-a-view-transition*/} -During a client-rendered [``](/reference/react/ViewTransition) update, React may wait for an image to load and decode before starting the animation. This applies when a new `` with a non-empty `src` is rendered, or when an existing image's `src` or `srcSet` changes. The image must be inside the `` subtree and must not have `loading="lazy"` or an `onLoad` handler. React does not wait for images during synchronous updates. +During a [``](/reference/react/ViewTransition) update, React waits up to 500 ms for visible images to load before starting the animation. This includes newly rendered `` elements and existing images whose `src` or `srcSet` changes. Setting `loading="lazy"` or adding an `onLoad` handler opts an image out. -When a Suspense boundary reveals streamed content inside a ``, React may also wait for visible images with a non-empty `src` that do not have `loading="lazy"`. React stops waiting after a timeout so that a slow image does not block the update indefinitely. - -In this example, the Suspense boundary is wrapped in a `` and shows a profile skeleton until the portrait has loaded. - -For comparison, the second button inserts the same card directly into the DOM. The card appears immediately, and the browser displays the image after it loads: +Compare how the same image appears when a Suspense boundary reveals its content inside and outside a ``: ```js -import { ViewTransition, Suspense, useState, startTransition } from 'react'; -import { freshImageUrl } from './image.js'; -import VanillaProfile from './VanillaProfile.js'; - -function Profile({ src }) { +import { + ViewTransition, + Suspense, + use, + useState, +} from 'react'; +import { fetchImageSrc } from './image.js'; + +function Profile({ cacheKey }) { + const src = use(fetchImageSrc(cacheKey)); return (
- Jack Pope +

Jack Pope

); @@ -159,56 +160,51 @@ function ProfilePlaceholder() { } export default function App() { - const [src, setSrc] = useState(null); + const [showWithTransition, setShowWithTransition] = + useState(false); + const [showWithoutTransition, setShowWithoutTransition] = + useState(false); return ( <> - - {src && ( + {showWithTransition && ( }> - + )}
- + + {showWithoutTransition && ( + }> + + + )} ); } ``` -```js src/VanillaProfile.js -import { useRef } from 'react'; -import { freshImageUrl } from './image.js'; +```js src/image.js hidden +// Normally, the caching logic would be inside a framework. +const cache = new Map(); -export default function VanillaProfile() { - const ref = useRef(null); - function show() { - ref.current.innerHTML = `
- Jack Pope -

Jack Pope

-
`; +export function fetchImageSrc(cacheKey) { + if (!cache.has(cacheKey)) { + cache.set(cacheKey, loadImageSrc()); } - return ( - <> - -
- - ); + return cache.get(cacheKey); } -``` -```js src/image.js hidden -// Add a unique parameter so the image isn't cached, -// and every run shows the loading state. -export function freshImageUrl() { +async function loadImageSrc() { + // Delay the response so the Suspense fallback is visible. + await new Promise(resolve => setTimeout(resolve, 1000)); + // Add a unique parameter so the image isn't cached. return 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now(); } ``` @@ -255,3 +251,5 @@ hr { ``` + +With ``, React keeps the skeleton visible for up to 500 ms while the image loads, so the card can be revealed with its image already in place. Without ``, Suspense stops showing the skeleton as soon as the Promise resolves. If the image is still loading, the card appears first and the image pops in afterward. diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index 18b20816f..179a4d368 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -47,8 +47,8 @@ A Suspense boundary waits for its content to be ready before revealing it. Any o - Reading a Promise with [`use`](/reference/react/use), including data streamed from [Server Components](/reference/rsc/server-components) or loaded through a [Suspense-enabled framework](#suspense-enabled-frameworks). - Loading a stylesheet rendered with [`` and a `precedence` prop.](/reference/react-dom/components/link#special-rendering-behavior) React blocks the boundary until the stylesheet loads, up to a timeout. [See an example below.](#waiting-for-a-stylesheet-to-load) - Waiting for a large boundary's HTML to arrive during streaming server rendering. Sending HTML takes time, so a boundary with enough content activates even when nothing in it suspends. React reveals the content as the HTML arrives. -- Loading fonts. Suspense doesn't wait for fonts by default, but a [``](/reference/react/ViewTransition) update waits for new fonts to load, up to a timeout, so text doesn't flash with a fallback font. [See an example below.](#waiting-for-a-font-to-load) -- Loading images. Suspense doesn't wait for images by default, but during a [``](/reference/react/ViewTransition) update, React blocks the boundary until the image loads, up to a timeout. Adding an `onLoad` handler opts a specific image out. [See an example below.](#waiting-for-an-image-to-load) +- Loading fonts. Suspense doesn't wait for fonts by default, but when a [``](/reference/react/ViewTransition) animates a boundary's reveal, React waits up to 500 ms for new fonts to load so text doesn't flash with a fallback font. [See an example below.](#waiting-for-a-font-to-load) +- Loading images. Suspense doesn't wait for images by default, but when a [``](/reference/react/ViewTransition) animates a boundary's reveal, React waits up to 500 ms for visible images to load before starting the animation. Setting `loading="lazy"` or adding an `onLoad` handler opts a specific image out. [See an example below.](#waiting-for-an-image-to-load) - Performing CPU-bound render work inside a [``](#props) boundary. @@ -2901,52 +2901,59 @@ Where you place the `` relative to the boundary determines wheth ### Waiting for a font to load {/*waiting-for-a-font-to-load*/} -When a [``](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React waits for new fonts the content introduces, up to a timeout, so the text doesn't flash with a fallback font. This only happens during a `` update. +When a [``](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React waits up to 500 ms for new fonts the content introduces so the text doesn't flash with a fallback font. This only happens during a `` update. -In the example below, the Suspense boundary is wrapped in a ``, and the `Quote` component suspends while its data loads. Rendering the quote starts its font download. React keeps the fallback visible until the font has loaded, so the quote appears already in its font. +In the example below, the Suspense boundary is wrapped in a ``, and the `Quote` component suspends while its data loads. Rendering the quote loads a stylesheet that introduces the font. React keeps the fallback visible while the stylesheet and font load. If the font loads within the 500 ms limit, the quote appears already in its font. For comparison, the second button performs the same update without React. Nothing waits for the font, so the text appears in a fallback font first and then switches: ```js -import { ViewTransition, Suspense, use, useState, startTransition } from 'react'; +import { + ViewTransition, + Suspense, + use, + useState, + startTransition, +} from 'react'; import { fetchQuote } from './data.js'; -import { freshFontUrl } from './font.js'; +import { + freshFontUrl, + freshStylesheetUrl, +} from './font.js'; import VanillaQuote from './VanillaQuote.js'; -function Quote({ fontSrc }) { +function Quote({ stylesheet }) { const quote = use(fetchQuote()); return ( <> - +

{quote}

); } export default function App() { - const [fontSrc, setFontSrc] = useState(null); + const [stylesheet, setStylesheet] = useState(null); return ( <> - {fontSrc && ( + {stylesheet && ( ⌛ Loading quote...

}> - +
)} @@ -2967,7 +2974,7 @@ export default function VanillaQuote() { const style = document.createElement('style'); style.textContent = `@font-face { font-family: 'VanillaFancy'; - src: url(${freshFontUrl()}) format('truetype'); + src: url(${freshFontUrl()}) format('woff2'); font-display: swap; }`; document.head.appendChild(style); @@ -2983,11 +2990,19 @@ export default function VanillaQuote() { ``` ```js src/font.js hidden -// Add a unique parameter so the font isn't cached, -// and every run shows the loading state. +export function freshStylesheetUrl() { + // Add a unique parameter so the stylesheet isn't cached. + return ( + 'https://fonts.googleapis.com/css2?family=Caveat&display=swap' + + '&t=' + + Date.now() + ); +} + export function freshFontUrl() { + // Add a unique parameter so the font isn't cached. return ( - 'https://raw.githubusercontent.com/google/fonts/main/ofl/caveat/Caveat%5Bwght%5D.ttf' + + 'https://fonts.gstatic.com/s/caveat/v23/WnznHAc5bAfYB2QRah7pcpNvOx-pjfJ9eIWpYT5Kmgq3sw.woff2' + '?t=' + Date.now() ); @@ -3025,7 +3040,7 @@ export function fetchQuote() { margin-top: 1em; } .fancy { - font-family: 'Fancy', sans-serif; + font-family: 'Caveat', sans-serif; } .vanilla-fancy { font-family: 'VanillaFancy', sans-serif; @@ -3038,8 +3053,8 @@ hr { ```json package.json hidden { "dependencies": { - "react": "19.3.0-canary-f1f7ed2a-20260904", - "react-dom": "19.3.0-canary-f1f7ed2a-20260904", + "react": "19.3.0", + "react-dom": "19.3.0", "react-scripts": "latest" } } @@ -3051,23 +3066,26 @@ hr { ### Waiting for an image to load {/*waiting-for-an-image-to-load*/} -When a [``](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React waits for visible images to load, up to a timeout, so the animation doesn't start with a half-loaded image. This only happens during a `` update. Adding an `onLoad` handler opts a specific image out, even inside a ``. - -In the example below, the Suspense boundary is wrapped in a `` and shows a profile skeleton until the portrait has loaded. +When a Suspense boundary reveals content inside a [``](/reference/react/ViewTransition), React waits up to 500 ms for visible images to load before starting the animation. Setting `loading="lazy"` or adding an `onLoad` handler opts an image out. -For comparison, the second button performs the same update without React. Nothing waits for the image, so the card appears immediately and the image pops in when it loads: +Compare the same boundary inside and outside a ``. Inside, React keeps the profile skeleton visible for up to 500 ms while the image loads, so the card can be revealed with its image already in place. Outside, Suspense stops showing the skeleton as soon as the Promise resolves. If the image is still loading, the card appears first and the image pops in afterward: ```js -import { ViewTransition, Suspense, useState, startTransition } from 'react'; -import { freshImageUrl } from './image.js'; -import VanillaProfile from './VanillaProfile.js'; +import { + ViewTransition, + Suspense, + use, + useState, +} from 'react'; +import { fetchImageSrc } from './image.js'; -function Profile({ src }) { +function Profile({ cacheKey }) { + const src = use(fetchImageSrc(cacheKey)); return (
- Jack Pope +

Jack Pope

); @@ -3083,56 +3101,51 @@ function ProfilePlaceholder() { } export default function App() { - const [src, setSrc] = useState(null); + const [showWithTransition, setShowWithTransition] = + useState(false); + const [showWithoutTransition, setShowWithoutTransition] = + useState(false); return ( <> - - {src && ( + {showWithTransition && ( }> - + )}
- + + {showWithoutTransition && ( + }> + + + )} ); } ``` -```js src/VanillaProfile.js -import { useRef } from 'react'; -import { freshImageUrl } from './image.js'; +```js src/image.js hidden +// Normally, the caching logic would be inside a framework. +const cache = new Map(); -export default function VanillaProfile() { - const ref = useRef(null); - function show() { - ref.current.innerHTML = `
- Jack Pope -

Jack Pope

-
`; +export function fetchImageSrc(cacheKey) { + if (!cache.has(cacheKey)) { + cache.set(cacheKey, loadImageSrc()); } - return ( - <> - -
- - ); + return cache.get(cacheKey); } -``` -```js src/image.js hidden -// Add a unique parameter so the image isn't cached, -// and every run shows the loading state. -export function freshImageUrl() { +async function loadImageSrc() { + // Delay the response so the Suspense fallback is visible. + await new Promise(resolve => setTimeout(resolve, 1000)); + // Add a unique parameter so the image isn't cached. return 'https://react.dev/images/team/jack-pope.jpg?t=' + Date.now(); } ``` @@ -3171,8 +3184,8 @@ hr { ```json package.json hidden { "dependencies": { - "react": "19.3.0-canary-f1f7ed2a-20260904", - "react-dom": "19.3.0-canary-f1f7ed2a-20260904", + "react": "19.3.0", + "react-dom": "19.3.0", "react-scripts": "latest" } } @@ -3202,7 +3215,7 @@ function ProfileCard({ resources }) { <>
- Jack Pope +

Jack Pope

{quote}

@@ -3262,7 +3275,6 @@ import { freshStylesheetUrl, freshImageUrl } from './resources.js'; export default function VanillaProfileCard() { const ref = useRef(null); async function show() { - const quote = await fetchQuote(); const doc = ref.current.contentWindow.document; doc.open(); doc.write(` @@ -3272,17 +3284,34 @@ export default function VanillaProfileCard() { .profile-card img { border-radius: 50%; background: #dfe3e9; } .name { margin: 0 0 4px; font-family: 'Caveat', sans-serif; font-size: 22px; line-height: 28px; font-weight: bold; } .bio { margin: 0; font-family: 'Caveat', sans-serif; font-size: 20px; line-height: 26px; } + .avatar-placeholder { width: 80px; height: 80px; border-radius: 50%; background: #dfe3e9; } + .name-placeholder, .bio-placeholder { border-radius: 4px; background: #dfe3e9; color: transparent; } + .name-placeholder { width: 90px; } + .bio-placeholder { width: 220px; height: 52px; }
- Jack Pope +
-

Jack Pope

-

${quote}

+

 

+

 

- `); doc.close(); + + const quote = await fetchQuote(); + doc.body.innerHTML = ` +
+ +
+

Jack Pope

+

${quote}

+
+
`; + const stylesheet = doc.createElement('link'); + stylesheet.rel = 'stylesheet'; + stylesheet.href = freshStylesheetUrl(); + doc.head.appendChild(stylesheet); } return ( <> @@ -3375,6 +3404,7 @@ hr { } .bio-placeholder { width: 220px; + height: 52px; } .vanilla-frame { display: block; @@ -3388,8 +3418,8 @@ hr { ```json package.json hidden { "dependencies": { - "react": "19.3.0-canary-f1f7ed2a-20260904", - "react-dom": "19.3.0-canary-f1f7ed2a-20260904", + "react": "19.3.0", + "react-dom": "19.3.0", "react-scripts": "latest" } } diff --git a/src/content/reference/react/ViewTransition.md b/src/content/reference/react/ViewTransition.md index 1011a1c96..17595bb76 100644 --- a/src/content/reference/react/ViewTransition.md +++ b/src/content/reference/react/ViewTransition.md @@ -1253,9 +1253,9 @@ It's important to properly use keys to preserve identity when reordering lists. ### Animating from Suspense content {/*animating-from-suspense-content*/} -Like any Transition, React waits for data and new CSS (``) before running the animation. In addition to this, ViewTransitions also wait up to 500ms for new fonts to load before starting the animation to avoid them flickering in later. For the same reason, an image wrapped in ViewTransition will wait for the image to load. See examples of [waiting for a font](/reference/react/Suspense#waiting-for-a-font-to-load) and [waiting for an image](/reference/react/Suspense#waiting-for-an-image-to-load) on the Suspense page. +Like any Transition, React waits for data and new CSS (``) before running the animation. `` also waits up to 500 ms for new fonts and visible images to load so they don't flicker in after the animation starts. See examples of [waiting for a font](/reference/react/Suspense#waiting-for-a-font-to-load) and [waiting for an image](/reference/react/Suspense#waiting-for-an-image-to-load) on the Suspense page. -If it's inside a new Suspense boundary instance, then the fallback is shown first. After the Suspense boundary fully loads, it triggers the `` to animate the reveal to the content. +If the content is inside a new Suspense boundary, React first shows the fallback. After the boundary finishes loading, `` animates the reveal to the content. There are two ways to animate Suspense boundaries depending on where you place the ``: @@ -1315,7 +1315,7 @@ export function VideoPlaceholder() { ``` ```js -import {ViewTransition, useState, startTransition, Suspense} from 'react'; +import { ViewTransition, Suspense, useState } from 'react'; import {Video, VideoPlaceholder} from './Video'; import {useLazyVideoData} from './data'; @@ -1328,12 +1328,7 @@ export default function Component() { const [showItem, setShowItem] = useState(false); return ( <> - {showItem ? ( @@ -1486,8 +1481,8 @@ button:hover { ```json package.json hidden { "dependencies": { - "react": "19.3.0-canary-f1f7ed2a-20260904", - "react-dom": "19.3.0-canary-f1f7ed2a-20260904", + "react": "19.3.0", + "react-dom": "19.3.0", "react-scripts": "latest" } } From ae5e48074d4f73f1e349eda2a66841f9a959023e Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Tue, 20 Oct 2026 14:16:54 +0100 Subject: [PATCH 4/4] Add React DevTools for agents reference page (#8694) --- .../dev-tools/react-devtools-cdt-mcp.md | 407 ++++++++++++++++++ src/sidebarReference.json | 5 + 2 files changed, 412 insertions(+) create mode 100644 src/content/reference/dev-tools/react-devtools-cdt-mcp.md diff --git a/src/content/reference/dev-tools/react-devtools-cdt-mcp.md b/src/content/reference/dev-tools/react-devtools-cdt-mcp.md new file mode 100644 index 000000000..9c966a0ff --- /dev/null +++ b/src/content/reference/dev-tools/react-devtools-cdt-mcp.md @@ -0,0 +1,407 @@ +--- +title: React DevTools for agents +version: experimental +--- + + + +**`react-devtools-cdt-mcp` is experimental.** It is built on the [experimental third-party developer tools API](https://developer.chrome.com/blog/devtools-for-agents-3p-tools) in [Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp), and both its tools and their output may change between releases. + + + + + +`react-devtools-cdt-mcp` gives AI coding agents the same view of your app that React DevTools gives you: the component tree, props, hooks, owners, source locations, and render timing. + +```js +import 'react-devtools-cdt-mcp/register'; +``` + + + + + +--- + +## Overview {/*overview*/} + +Coding agents that drive a browser through [Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp) can click, take snapshots, and read the DOM, but the DOM doesn't tell them which component rendered a button, what state it holds, or why an interaction was slow. + +`react-devtools-cdt-mcp` is a small browser library that you import into the page you're debugging. It registers a group of React tools with the page. When Chrome DevTools MCP connects to that page, it discovers the tools automatically, and your agent can call them like any other MCP tool. + + + +`react-devtools-cdt-mcp` is **not** an MCP server, and it doesn't go in your MCP client config. It runs inside your page. The only MCP server you configure is Chrome DevTools MCP. + + + +--- + +## Setup {/*setup*/} + +#### Install the package {/*install-the-package*/} + + + +npm install --save-dev react-devtools-cdt-mcp + + + +#### Import it before React {/*import-it-before-react*/} + +Import the `register` entry point at the very top of your app's entry file, **before** anything imports `react-dom`: + +```js {1} +import 'react-devtools-cdt-mcp/register'; + +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')).render(); +``` + +`react-devtools-cdt-mcp` has to load before React does. If you import it after React, the tools won't see your components. + + + +`react-devtools-cdt-mcp` connects to React the same way the React DevTools extension does, and makes React report every commit to it. This slows down your app, so only include it in development builds. + + + +#### Set up Chrome DevTools MCP {/*set-up-chrome-devtools-mcp*/} + +Your agent needs [Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp) with [third-party developer tools](https://developer.chrome.com/blog/devtools-for-agents-3p-tools) enabled. Follow the [Chrome DevTools MCP docs](https://github.com/ChromeDevTools/chrome-devtools-mcp#getting-started) to set it up. Once your agent opens a page that imports `react-devtools-cdt-mcp/register`, it can list and call the React tools. + +--- + +## Try it {/*try-it*/} + +You don't need an agent to see what the tools return. The sandbox below imports `react-devtools-cdt-mcp/register` exactly as you would in your app, and adds a small **tool console** under the app. The console discovers the React tools the same way Chrome DevTools MCP does, by dispatching the `devtoolstooldiscovery` event, and lets you call them by hand. + +Pick a tool, edit the JSON parameters, and press **Run tool**. Here's a debugging session to follow, playing the agent's part: + +1. **Find a component.** Run `react_find_components` with `{"name": "Row"}`. Each result has a `uid`, like `"r12"`, that the other tools accept. +2. **Inspect it.** Run `react_get_component_by_uid` with one of those uids and `"includeHooks": true` to see its props and hooks. +3. **Start from the screen.** Choose `react_get_component_by_dom_element`, press **Pick element**, and click a product in the list to get the host component that rendered it. +4. **Profile an interaction.** Run `react_start_profiling`, click **Add to cart** in the app, then run `react_stop_profiling`. +5. **See what rendered.** Run `react_get_trace_overview`, then `react_get_commit_report` for commit `0` to see which components rendered in that commit and how long each one took. + + + +```js src/App.js active +import { useState } from 'react'; + +const PRODUCTS = Array.from({ length: 20 }, (_, i) => ({ + id: i, + name: 'Product ' + (i + 1), + price: 10 + i, +})); + +export default function App() { + const [query, setQuery] = useState(''); + const visibleProducts = PRODUCTS.filter((product) => + product.name.toLowerCase().includes(query.toLowerCase()) + ); + return ( +
+ + setQuery(e.target.value)} + placeholder="Filter products" + /> + +
+ ); +} + +function Cart() { + const [count, setCount] = useState(0); + return ( +
+ + +
+ ); +} + +function CartBadge({ count }) { + return Cart: {count}; +} + +function ProductList({ products }) { + return ( +
    + {products.map((product) => ( + + ))} +
+ ); +} + +function ProductRow({ product }) { + return ( +
  • + {product.name} ${product.price} +
  • + ); +} +``` + +```js src/index.js +import 'react-devtools-cdt-mcp/register'; + +import { createRoot } from 'react-dom/client'; +import './styles.css'; +import App from './App'; +import { mountToolConsole } from './ToolConsole'; + +createRoot(document.getElementById('root')).render(); +mountToolConsole(); +``` + +```js src/ToolConsole.js hidden +// A tiny stand-in for chrome-devtools-mcp. It discovers the React tool group +// the same way chrome-devtools-mcp does (the `devtoolstooldiscovery` event) +// and lets you call the tools by hand. +function discoverToolGroup() { + let toolGroup = null; + const event = new Event('devtoolstooldiscovery'); + event.respondWith = group => { + toolGroup = group; + }; + window.dispatchEvent(event); + return toolGroup; +} + +function firstUid(tools, name) { + const find = tools.find(t => t.name === 'react_find_components'); + const result = find.execute({name}); + return result.results && result.results[0] ? result.results[0].uid : 'r0'; +} + +function exampleParams(tools, toolName) { + switch (toolName) { + case 'react_get_component_tree': + return {depth: 4}; + case 'react_find_components': + return {name: 'Row'}; + case 'react_get_component_by_uid': + return {uid: firstUid(tools, 'App'), includeHooks: true}; + case 'react_get_component_by_dom_element': + return {element: '(click "Pick element", then click the page)'}; + case 'react_get_component_source': + case 'react_get_owner_stack_trace': + case 'react_get_parent_stack': + case 'react_get_owner_stack': + return {uid: firstUid(tools, 'CartBadge')}; + case 'react_start_profiling': + return {traceName: 'add-to-cart'}; + case 'react_stop_profiling': + return {}; + case 'react_get_trace_overview': + return {traceName: 'add-to-cart'}; + case 'react_get_commit_report': + return {traceName: 'add-to-cart', commitIndex: 0}; + default: + return {}; + } +} + +export function mountToolConsole() { + const toolGroup = discoverToolGroup(); + const container = document.createElement('section'); + container.className = 'tool-console'; + document.body.appendChild(container); + if (toolGroup === null) { + container.textContent = + 'No tools found. Import react-devtools-cdt-mcp/register before React.'; + return; + } + const {tools} = toolGroup; + container.innerHTML = ` +

    Tool console

    + +

    + +
    + + +
    +
    
    +  `;
    +  const select = container.querySelector('select');
    +  const description = container.querySelector('.description');
    +  const textarea = container.querySelector('textarea');
    +  const runButton = container.querySelector('.run');
    +  const pickButton = container.querySelector('.pick');
    +  const output = container.querySelector('pre');
    +  let pickedElement = null;
    +
    +  for (const tool of tools) {
    +    const option = document.createElement('option');
    +    option.value = tool.name;
    +    option.textContent = tool.name;
    +    select.appendChild(option);
    +  }
    +
    +  function selectTool() {
    +    const tool = tools.find(t => t.name === select.value);
    +    description.textContent = tool.description;
    +    textarea.value = JSON.stringify(exampleParams(tools, tool.name));
    +    pickButton.hidden = tool.name !== 'react_get_component_by_dom_element';
    +    pickedElement = null;
    +  }
    +
    +  function run() {
    +    const tool = tools.find(t => t.name === select.value);
    +    let params;
    +    try {
    +      params = JSON.parse(textarea.value || '{}');
    +    } catch (error) {
    +      output.textContent = 'Invalid JSON: ' + error.message;
    +      return;
    +    }
    +    if (tool.name === 'react_get_component_by_dom_element') {
    +      params.element = pickedElement;
    +    }
    +    const result = tool.execute(params);
    +    output.textContent = JSON.stringify(result, null, 2);
    +  }
    +
    +  pickButton.addEventListener('click', () => {
    +    output.textContent = 'Click any element in the app above...';
    +    document.addEventListener(
    +      'click',
    +      event => {
    +        if (container.contains(event.target)) {
    +          return;
    +        }
    +        event.preventDefault();
    +        event.stopPropagation();
    +        pickedElement = event.target;
    +        textarea.value = JSON.stringify({
    +          element: '<' + event.target.tagName.toLowerCase() + '>',
    +        });
    +        run();
    +      },
    +      {capture: true, once: true},
    +    );
    +  });
    +  select.addEventListener('change', selectTool);
    +  runButton.addEventListener('click', run);
    +  selectTool();
    +}
    +```
    +
    +```css src/styles.css hidden
    +header { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
    +.badge { padding: 2px 8px; border-radius: 10px; background: #087ea4; color: white; }
    +ul { padding-left: 20px; max-height: 160px; overflow: auto; }
    +.tool-console { margin-top: 20px; padding-top: 12px; border-top: 1px solid #ddd; }
    +.tool-console select, .tool-console textarea { width: 100%; font-family: monospace; }
    +.tool-console .actions { display: flex; gap: 8px; margin: 8px 0; }
    +.tool-console pre { background: #f6f7f9; padding: 8px; max-height: 300px; overflow: auto; }
    +```
    +
    +```json package.json hidden
    +{
    +  "dependencies": {
    +    "react": "latest",
    +    "react-dom": "latest",
    +    "react-scripts": "latest",
    +    "react-devtools-cdt-mcp": "0.0.1"
    +  },
    +  "scripts": {
    +    "start": "react-scripts start",
    +    "build": "react-scripts build",
    +    "test": "react-scripts test --env=jsdom",
    +    "eject": "react-scripts eject"
    +  }
    +}
    +```
    +
    +
    + + + +The tool console in this sandbox stands in for Chrome DevTools MCP so you can try the tools on this page. In your own app, you don't write any of this code: Chrome DevTools MCP discovers the tools and your agent decides which ones to call. + + + +--- + +## Reference {/*reference*/} + +### Tools {/*tools*/} + +`react-devtools-cdt-mcp` registers a tool group called `react`. Your agent calls these tools through Chrome DevTools MCP. + +* `react_get_component_tree`: Returns a snapshot of the component tree. +* `react_find_components`: Finds components by name. +* `react_get_component_by_uid`: Returns a component's props and Hooks. +* `react_get_component_by_dom_element`: Returns the React host component for a DOM element. +* `react_get_parent_stack`: Returns where a component is mounted in the tree. +* `react_get_owner_stack`: Returns the components that created a component in their JSX. +* `react_get_owner_stack_trace`: Returns the owner stack of a component as a stack trace. +* `react_get_component_source`: Returns where a component is defined. +* `react_start_profiling`: Starts recording render timing. +* `react_stop_profiling`: Stops recording render timing. +* `react_get_trace_overview`: Returns a summary of every commit in a recording. +* `react_get_commit_report`: Returns which components rendered in a commit and how long each took. + +Components are identified by short, stable uids like `"r5"`. A uid stays the same across every tool and across re-renders, but not across page reloads. + +Every tool returns a plain, serializable object. If a tool fails, it returns an object with an `error` message instead. + +--- + +### `register(target?)` {/*register*/} + +Call `register` to install the React DevTools hook and register the React tools with Chrome DevTools MCP yourself, instead of importing `react-devtools-cdt-mcp/register`. + +```js +import { register } from 'react-devtools-cdt-mcp'; + +register(); +``` + +#### Parameters {/*register-parameters*/} + +* **optional** `target`: The global object to install the hook on. Defaults to `globalThis`. + +#### Returns {/*register-returns*/} + +`register` returns an object with two properties: + +* `facade`: The installed React DevTools hook and the state the tools read from. +* `unregister`: A function that stops registering the React tools with Chrome DevTools MCP. + +#### Caveats {/*register-caveats*/} + +* Call `register` before React initializes, or React won't connect to the hook. +* Calling `register` again for the same `target` returns the existing registration. + +--- + +## Troubleshooting {/*troubleshooting*/} + +#### My agent doesn't see any React tools {/*my-agent-doesnt-see-any-react-tools*/} + +Check that [third-party developer tools](https://developer.chrome.com/blog/devtools-for-agents-3p-tools) are enabled in your Chrome DevTools MCP setup. Then check that the page selected in Chrome DevTools MCP imports `react-devtools-cdt-mcp/register`. The tools are discovered per page, when the page loads or is selected. + +#### The component tree is empty {/*the-component-tree-is-empty*/} + +`react-devtools-cdt-mcp/register` was imported after React initialized. Move the import to the very first line of your entry file, before any import that loads `react-dom`. + +#### I'm getting an error about a browser-like environment {/*im-getting-an-error-about-a-browser-like-environment*/} + +`react-devtools-cdt-mcp/register` throws if it's imported outside a browser, for example during server rendering or in tests. Import it only from client-side code, or call `register()` from the package root yourself where it's safe. + +#### Source locations and owner stacks are empty {/*source-locations-and-owner-stacks-are-empty*/} + +This data only exists in development builds of React. Make sure you're running your app's development server rather than a production build. + +#### A uid stopped working {/*a-uid-stopped-working*/} + +Uids don't survive page reloads. After a reload, get fresh uids with `react_get_component_tree` or `react_find_components`. diff --git a/src/sidebarReference.json b/src/sidebarReference.json index 1fbb5a06a..be0b0c183 100644 --- a/src/sidebarReference.json +++ b/src/sidebarReference.json @@ -410,6 +410,11 @@ "title": "React Performance tracks", "path": "/reference/dev-tools/react-performance-tracks" }, + { + "title": "React DevTools for agents", + "path": "/reference/dev-tools/react-devtools-cdt-mcp", + "version": "experimental" + }, { "hasSectionHeader": true, "sectionHeader": "eslint-plugin-react-hooks"