From 7385d1f61ac6fedb1473ee5aafb2c39d1a620a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 16 Oct 2025 10:49:23 -0400 Subject: [PATCH 01/11] [DevTools] Add inspection button to Suspense tab (#34867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add inspection button to Suspense tab which lets you select only among Suspense nodes. It highlights all the DOM nodes in the root of the Suspense node instead of just the DOM element you hover. The name is inferred. Screenshot 2025-10-15 at 8 03 34 PM --- .../src/__tests__/store-test.js | 2 +- .../src/backend/agent.js | 100 ++++++------------ .../src/backend/fiber/renderer.js | 46 +++++++- .../src/backend/flight/renderer.js | 3 + .../src/backend/legacy/renderer.js | 3 + .../src/backend/types.js | 1 + .../src/backend/views/Highlighter/index.js | 44 ++++++-- packages/react-devtools-shared/src/bridge.js | 2 +- .../Components/InspectHostNodesToggle.js | 8 +- .../devtools/views/SuspenseTab/SuspenseTab.js | 9 ++ 10 files changed, 140 insertions(+), 78 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 3638852c20b72..c02d8130c308e 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -1546,7 +1546,7 @@ describe('Store', () => { ▸ `); - const deepestedNodeID = agent.getIDForHostInstance(ref.current); + const deepestedNodeID = agent.getIDForHostInstance(ref.current).id; await act(() => store.toggleIsCollapsed(deepestedNodeID, false)); expect(store).toMatchInlineSnapshot(` diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index fce8fe626d443..42fbbc9648a98 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -455,7 +455,10 @@ export default class Agent extends EventEmitter<{ return renderer.getInstanceAndStyle(id); } - getIDForHostInstance(target: HostInstance): number | null { + getIDForHostInstance( + target: HostInstance, + onlySuspenseNodes?: boolean, + ): null | {id: number, rendererID: number} { if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') { // In React Native or non-DOM we simply pick any renderer that has a match. for (const rendererID in this._rendererInterfaces) { @@ -463,9 +466,14 @@ export default class Agent extends EventEmitter<{ (rendererID: any) ]: any): RendererInterface); try { - const match = renderer.getElementIDForHostInstance(target); - if (match != null) { - return match; + const id = onlySuspenseNodes + ? renderer.getSuspenseNodeIDForHostInstance(target) + : renderer.getElementIDForHostInstance(target); + if (id !== null) { + return { + id: id, + rendererID: +rendererID, + }; } } catch (error) { // Some old React versions might throw if they can't find a match. @@ -478,6 +486,7 @@ export default class Agent extends EventEmitter<{ // that is registered if there isn't an exact match. let bestMatch: null | Element = null; let bestRenderer: null | RendererInterface = null; + let bestRendererID: number = 0; // Find the nearest ancestor which is mounted by a React. for (const rendererID in this._rendererInterfaces) { const renderer = ((this._rendererInterfaces[ @@ -491,6 +500,7 @@ export default class Agent extends EventEmitter<{ // Exact match we can exit early. bestMatch = nearestNode; bestRenderer = renderer; + bestRendererID = +rendererID; break; } if (bestMatch === null || bestMatch.contains(nearestNode)) { @@ -498,12 +508,21 @@ export default class Agent extends EventEmitter<{ // so the new match is a deeper and therefore better match. bestMatch = nearestNode; bestRenderer = renderer; + bestRendererID = +rendererID; } } } if (bestRenderer != null && bestMatch != null) { try { - return bestRenderer.getElementIDForHostInstance(bestMatch); + const id = onlySuspenseNodes + ? bestRenderer.getSuspenseNodeIDForHostInstance(bestMatch) + : bestRenderer.getElementIDForHostInstance(bestMatch); + if (id !== null) { + return { + id, + rendererID: bestRendererID, + }; + } } catch (error) { // Some old React versions might throw if they can't find a match. // If so we should ignore it... @@ -514,65 +533,14 @@ export default class Agent extends EventEmitter<{ } getComponentNameForHostInstance(target: HostInstance): string | null { - // We duplicate this code from getIDForHostInstance to avoid an object allocation. - if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') { - // In React Native or non-DOM we simply pick any renderer that has a match. - for (const rendererID in this._rendererInterfaces) { - const renderer = ((this._rendererInterfaces[ - (rendererID: any) - ]: any): RendererInterface); - try { - const id = renderer.getElementIDForHostInstance(target); - if (id) { - return renderer.getDisplayNameForElementID(id); - } - } catch (error) { - // Some old React versions might throw if they can't find a match. - // If so we should ignore it... - } - } - return null; - } else { - // In the DOM we use a smarter mechanism to find the deepest a DOM node - // that is registered if there isn't an exact match. - let bestMatch: null | Element = null; - let bestRenderer: null | RendererInterface = null; - // Find the nearest ancestor which is mounted by a React. - for (const rendererID in this._rendererInterfaces) { - const renderer = ((this._rendererInterfaces[ - (rendererID: any) - ]: any): RendererInterface); - const nearestNode: null | Element = renderer.getNearestMountedDOMNode( - (target: any), - ); - if (nearestNode !== null) { - if (nearestNode === target) { - // Exact match we can exit early. - bestMatch = nearestNode; - bestRenderer = renderer; - break; - } - if (bestMatch === null || bestMatch.contains(nearestNode)) { - // If this is the first match or the previous match contains the new match, - // so the new match is a deeper and therefore better match. - bestMatch = nearestNode; - bestRenderer = renderer; - } - } - } - if (bestRenderer != null && bestMatch != null) { - try { - const id = bestRenderer.getElementIDForHostInstance(bestMatch); - if (id) { - return bestRenderer.getDisplayNameForElementID(id); - } - } catch (error) { - // Some old React versions might throw if they can't find a match. - // If so we should ignore it... - } - } - return null; + const match = this.getIDForHostInstance(target); + if (match !== null) { + const renderer = ((this._rendererInterfaces[ + (match.rendererID: any) + ]: any): RendererInterface); + return renderer.getDisplayNameForElementID(match.id); } + return null; } getBackendVersion: () => void = () => { @@ -971,9 +939,9 @@ export default class Agent extends EventEmitter<{ }; selectNode(target: HostInstance): void { - const id = this.getIDForHostInstance(target); - if (id !== null) { - this._bridge.send('selectElement', id); + const match = this.getIDForHostInstance(target); + if (match !== null) { + this._bridge.send('selectElement', match.id); } } diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index b7fe41b96c5b4..47902a4438244 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -5793,7 +5793,28 @@ export function attach( return null; } if (devtoolsInstance.kind === FIBER_INSTANCE) { - return getDisplayNameForFiber(devtoolsInstance.data); + const fiber = devtoolsInstance.data; + if (fiber.tag === HostRoot) { + // The only reason you'd inspect a HostRoot is to show it as a SuspenseNode. + return 'Initial Paint'; + } + if (fiber.tag === SuspenseComponent || fiber.tag === ActivityComponent) { + // For Suspense and Activity components, we can show a better name + // by using the name prop or their owner. + const props = fiber.memoizedProps; + if (props.name != null) { + return props.name; + } + const owner = getUnfilteredOwner(fiber); + if (owner != null) { + if (typeof owner.tag === 'number') { + return getDisplayNameForFiber((owner: any)); + } else { + return owner.name || ''; + } + } + } + return getDisplayNameForFiber(fiber); } else { return devtoolsInstance.data.name || ''; } @@ -5834,6 +5855,28 @@ export function attach( return null; } + function getSuspenseNodeIDForHostInstance( + publicInstance: HostInstance, + ): number | null { + const instance = publicInstanceToDevToolsInstanceMap.get(publicInstance); + if (instance !== undefined) { + // Pick nearest unfiltered SuspenseNode instance. + let suspenseInstance = instance; + while ( + suspenseInstance.suspenseNode === null || + suspenseInstance.kind === FILTERED_FIBER_INSTANCE + ) { + if (suspenseInstance.parent === null) { + // We shouldn't get here since we'll always have a suspenseNode at the root. + return null; + } + suspenseInstance = suspenseInstance.parent; + } + return suspenseInstance.id; + } + return null; + } + function getElementAttributeByPath( id: number, path: Array, @@ -8630,6 +8673,7 @@ export function attach( getDisplayNameForElementID, getNearestMountedDOMNode, getElementIDForHostInstance, + getSuspenseNodeIDForHostInstance, getInstanceAndStyle, getOwnersList, getPathForElement, diff --git a/packages/react-devtools-shared/src/backend/flight/renderer.js b/packages/react-devtools-shared/src/backend/flight/renderer.js index d0dc9094334eb..e26525a0d607a 100644 --- a/packages/react-devtools-shared/src/backend/flight/renderer.js +++ b/packages/react-devtools-shared/src/backend/flight/renderer.js @@ -169,6 +169,9 @@ export function attach( getElementIDForHostInstance() { return null; }, + getSuspenseNodeIDForHostInstance() { + return null; + }, getInstanceAndStyle() { return { instance: null, diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 1262a8d4647a6..ccd9cdac3e02e 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -1269,6 +1269,9 @@ export function attach( getDisplayNameForElementID, getNearestMountedDOMNode, getElementIDForHostInstance, + getSuspenseNodeIDForHostInstance(id: number): null { + return null; + }, getInstanceAndStyle, findHostInstancesForElementID: (id: number) => { const hostInstance = findHostInstanceForInternalID(id); diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 1052dc9d75b14..67d6a5f834bc8 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -427,6 +427,7 @@ export type RendererInterface = { getComponentStack?: GetComponentStack, getNearestMountedDOMNode: (component: Element) => Element | null, getElementIDForHostInstance: GetElementIDForHostInstance, + getSuspenseNodeIDForHostInstance: GetElementIDForHostInstance, getDisplayNameForElementID: GetDisplayNameForElementID, getInstanceAndStyle(id: number): InstanceAndStyle, getProfilingData(): ProfilingDataBackend, diff --git a/packages/react-devtools-shared/src/backend/views/Highlighter/index.js b/packages/react-devtools-shared/src/backend/views/Highlighter/index.js index 894c4fba94404..b67b3964ed597 100644 --- a/packages/react-devtools-shared/src/backend/views/Highlighter/index.js +++ b/packages/react-devtools-shared/src/backend/views/Highlighter/index.js @@ -20,6 +20,7 @@ import type {RendererInterface} from '../../types'; // That is done by the React Native Inspector component. let iframesListeningTo: Set = new Set(); +let inspectOnlySuspenseNodes = false; export default function setupHighlighter( bridge: BackendBridge, @@ -33,7 +34,8 @@ export default function setupHighlighter( bridge.addListener('startInspectingHost', startInspectingHost); bridge.addListener('stopInspectingHost', stopInspectingHost); - function startInspectingHost() { + function startInspectingHost(onlySuspenseNodes: boolean) { + inspectOnlySuspenseNodes = onlySuspenseNodes; registerListenersOnWindow(window); } @@ -363,9 +365,37 @@ export default function setupHighlighter( } } - // Don't pass the name explicitly. - // It will be inferred from DOM tag and Fiber owner. - showOverlay([target], null, agent, false); + if (inspectOnlySuspenseNodes) { + // For Suspense nodes we want to highlight not the actual target but the nodes + // that are the root of the Suspense node. + // TODO: Consider if we should just do the same for other elements because the + // hovered node might just be one child of many in the Component. + const match = agent.getIDForHostInstance( + target, + inspectOnlySuspenseNodes, + ); + if (match !== null) { + const renderer = agent.rendererInterfaces[match.rendererID]; + if (renderer == null) { + console.warn( + `Invalid renderer id "${match.rendererID}" for element "${match.id}"`, + ); + return; + } + highlightHostInstance({ + displayName: renderer.getDisplayNameForElementID(match.id), + hideAfterTimeout: false, + id: match.id, + openBuiltinElementsPanel: false, + rendererID: match.rendererID, + scrollIntoView: false, + }); + } + } else { + // Don't pass the name explicitly. + // It will be inferred from DOM tag and Fiber owner. + showOverlay([target], null, agent, false); + } } function onPointerUp(event: MouseEvent) { @@ -374,9 +404,9 @@ export default function setupHighlighter( } const selectElementForNode = (node: HTMLElement) => { - const id = agent.getIDForHostInstance(node); - if (id !== null) { - bridge.send('selectElement', id); + const match = agent.getIDForHostInstance(node, inspectOnlySuspenseNodes); + if (match !== null) { + bridge.send('selectElement', match.id); } }; diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index 3162dc215ff0a..683b341920244 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -266,7 +266,7 @@ type FrontendEvents = { savedPreferences: [SavedPreferencesParams], setTraceUpdatesEnabled: [boolean], shutdown: [], - startInspectingHost: [], + startInspectingHost: [boolean], startProfiling: [StartProfilingParams], stopInspectingHost: [], scrollToHostInstance: [ScrollToHostInstance], diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js index 17a7b049cc9b3..1b8fd54dc5ce0 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js @@ -14,7 +14,11 @@ import Toggle from '../Toggle'; import ButtonIcon from '../ButtonIcon'; import {logEvent} from 'react-devtools-shared/src/Logger'; -export default function InspectHostNodesToggle(): React.Node { +export default function InspectHostNodesToggle({ + onlySuspenseNodes, +}: { + onlySuspenseNodes?: boolean, +}): React.Node { const [isInspecting, setIsInspecting] = useState(false); const bridge = useContext(BridgeContext); @@ -24,7 +28,7 @@ export default function InspectHostNodesToggle(): React.Node { if (isChecked) { logEvent({event_name: 'inspect-element-button-clicked'}); - bridge.send('startInspectingHost'); + bridge.send('startInspectingHost', !!onlySuspenseNodes); } else { bridge.send('stopInspectingHost'); } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js index 9ade19c33075b..1344faf0a29a8 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js @@ -14,6 +14,7 @@ import { useLayoutEffect, useReducer, useRef, + Fragment, } from 'react'; import { @@ -21,6 +22,7 @@ import { localStorageSetItem, } from 'react-devtools-shared/src/storage'; import ButtonIcon, {type IconType} from '../ButtonIcon'; +import InspectHostNodesToggle from '../Components/InspectHostNodesToggle'; import InspectedElementErrorBoundary from '../Components/InspectedElementErrorBoundary'; import InspectedElement from '../Components/InspectedElement'; import portaledContent from '../portaledContent'; @@ -156,6 +158,7 @@ function ToggleInspectedElement({ } function SuspenseTab(_: {}) { + const store = useContext(StoreContext); const {hideSettings} = useContext(OptionsContext); const [state, dispatch] = useReducer( layoutReducer, @@ -367,6 +370,12 @@ function SuspenseTab(_: {}) { ) : ( )} + {store.supportsClickToInspect && ( + + +
+ + )}
From 7bd8716acdd9a484e81addcaa923e0a69e61d533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 16 Oct 2025 10:49:37 -0400 Subject: [PATCH 02/11] [DevTools] Don't try to load anonymous or empty urls (#34869) This triggers unnecessary fetches. --- .../parseHookNames/loadSourceAndMetadata.js | 32 ++++++++++--------- .../src/symbolicateSource.js | 3 ++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js index 6d3f1223f6f6c..7c40cabf97fa3 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js @@ -475,23 +475,25 @@ function loadSourceFiles( const fetchPromise = dedupedFetchPromises.get(runtimeSourceURL) || - fetchFileFunction(runtimeSourceURL).then(runtimeSourceCode => { - // TODO (named hooks) Re-think this; the main case where it matters is when there's no source-maps, - // because then we need to parse the full source file as an AST. - if (runtimeSourceCode.length > MAX_SOURCE_LENGTH) { - throw Error('Source code too large to parse'); - } + (runtimeSourceURL && !runtimeSourceURL.startsWith(' { + // TODO (named hooks) Re-think this; the main case where it matters is when there's no source-maps, + // because then we need to parse the full source file as an AST. + if (runtimeSourceCode.length > MAX_SOURCE_LENGTH) { + throw Error('Source code too large to parse'); + } - if (__DEBUG__) { - console.groupCollapsed( - `loadSourceFiles() runtimeSourceURL "${runtimeSourceURL}"`, - ); - console.log(runtimeSourceCode); - console.groupEnd(); - } + if (__DEBUG__) { + console.groupCollapsed( + `loadSourceFiles() runtimeSourceURL "${runtimeSourceURL}"`, + ); + console.log(runtimeSourceCode); + console.groupEnd(); + } - return runtimeSourceCode; - }); + return runtimeSourceCode; + }) + : Promise.reject(new Error('Empty url'))); dedupedFetchPromises.set(runtimeSourceURL, fetchPromise); setterPromises.push( diff --git a/packages/react-devtools-shared/src/symbolicateSource.js b/packages/react-devtools-shared/src/symbolicateSource.js index 092b1f8187ad1..ebcf161dc963a 100644 --- a/packages/react-devtools-shared/src/symbolicateSource.js +++ b/packages/react-devtools-shared/src/symbolicateSource.js @@ -52,6 +52,9 @@ export async function symbolicateSource( lineNumber: number, // 1-based columnNumber: number, // 1-based ): Promise { + if (!sourceURL || sourceURL.startsWith(' null); if (resource == null) { return null; From 4e0074737826a1296eb028c53ce4f6b6db8d09ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 16 Oct 2025 10:50:18 -0400 Subject: [PATCH 03/11] [DevTools] Don't pluralize if already plural (#34870) In a demo today, `cookies()` showed up as `cookieses`. While adorable, is wrong. --- .../src/devtools/views/utils.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/react-devtools-shared/src/devtools/views/utils.js b/packages/react-devtools-shared/src/devtools/views/utils.js index 3b0de4118a29d..a2d254bc27d89 100644 --- a/packages/react-devtools-shared/src/devtools/views/utils.js +++ b/packages/react-devtools-shared/src/devtools/views/utils.js @@ -205,6 +205,27 @@ export function pluralize(word: string): string { return word; } + // Bail out if it's already plural. + switch (word) { + case 'men': + case 'women': + case 'children': + case 'feet': + case 'teeth': + case 'mice': + case 'people': + return word; + } + + if ( + /(ches|shes|ses|xes|zes)$/i.test(word) || + /[^s]ies$/i.test(word) || + /ves$/i.test(word) || + /[^s]s$/i.test(word) + ) { + return word; + } + switch (word) { case 'man': return 'men'; From 03ba0c76e15967fe0ac0413e9a8a55eb55f9e55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 16 Oct 2025 10:50:41 -0400 Subject: [PATCH 04/11] [DevTools] Include some sub-pixel precision in rects (#34873) Currently the sub-pixel precision is lost which can lead to things not lining up properly and being slightly off or overlapping. We need some sub-pixel precision. Ideally we'd just keep the floating point as is. I'm not sure why the operations is limited to integers. We don't send it as a typed array anyway it seems which would ideally be more optimal. Even if we did, we haven't defined a precision for the protocol. Is it 32bit integer? 64bit? If it's 64bit we can fit a float anyway. Ideally it would be more variable precision like just pushing into a typed array directly with the option to write whatever precision we want. --- .../src/backend/fiber/renderer.js | 16 ++++++++-------- .../react-devtools-shared/src/devtools/store.js | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 47902a4438244..de178909b29ad 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -2693,10 +2693,10 @@ export function attach( pushOperation(rects.length); for (let i = 0; i < rects.length; ++i) { const rect = rects[i]; - pushOperation(Math.round(rect.x)); - pushOperation(Math.round(rect.y)); - pushOperation(Math.round(rect.width)); - pushOperation(Math.round(rect.height)); + pushOperation(Math.round(rect.x * 1000)); + pushOperation(Math.round(rect.y * 1000)); + pushOperation(Math.round(rect.width * 1000)); + pushOperation(Math.round(rect.height * 1000)); } } } @@ -2765,10 +2765,10 @@ export function attach( pushOperation(rects.length); for (let i = 0; i < rects.length; ++i) { const rect = rects[i]; - pushOperation(Math.round(rect.x)); - pushOperation(Math.round(rect.y)); - pushOperation(Math.round(rect.width)); - pushOperation(Math.round(rect.height)); + pushOperation(Math.round(rect.x * 1000)); + pushOperation(Math.round(rect.y * 1000)); + pushOperation(Math.round(rect.width * 1000)); + pushOperation(Math.round(rect.height * 1000)); } } } diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 86961f5bd91fb..f1aa61bfe9b86 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -1587,10 +1587,10 @@ export default class Store extends EventEmitter<{ } else { rects = []; for (let rectIndex = 0; rectIndex < numRects; rectIndex++) { - const x = operations[i + 0]; - const y = operations[i + 1]; - const width = operations[i + 2]; - const height = operations[i + 3]; + const x = operations[i + 0] / 1000; + const y = operations[i + 1] / 1000; + const width = operations[i + 2] / 1000; + const height = operations[i + 3] / 1000; rects.push({x, y, width, height}); i += 4; } @@ -1763,10 +1763,10 @@ export default class Store extends EventEmitter<{ } else { nextRects = []; for (let rectIndex = 0; rectIndex < numRects; rectIndex++) { - const x = operations[i + 0]; - const y = operations[i + 1]; - const width = operations[i + 2]; - const height = operations[i + 3]; + const x = operations[i + 0] / 1000; + const y = operations[i + 1] / 1000; + const width = operations[i + 2] / 1000; + const height = operations[i + 3] / 1000; nextRects.push({x, y, width, height}); From d8aa94b0f4f22aadd4762b7ca1c690d3d85ae776 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Thu, 16 Oct 2025 18:00:41 +0200 Subject: [PATCH 05/11] Only capture stacks for up to 10 frames for Owner Stacks (#34864) --- packages/react/src/jsx/ReactJSXElement.js | 73 ++++++++++++++++++----- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js index a77c4c3cdbf10..e23c998da511b 100644 --- a/packages/react/src/jsx/ReactJSXElement.js +++ b/packages/react/src/jsx/ReactJSXElement.js @@ -57,6 +57,14 @@ function getOwner() { return null; } +// v8 (Chromium, Node.js) defaults to 10 +// SpiderMonkey (Firefox) does not support Error.stackTraceLimit +// JSC (Safari) defaults to 100 +// The lower the limit, the more likely we'll not reach react_stack_bottom_frame +// The higher the limit, the slower Error() is when not inspecting with a debugger. +// When inspecting with a debugger, Error.stackTraceLimit has no impact on Error() performance (in v8). +const ownerStackTraceLimit = 10; + /** @noinline */ function UnknownOwner() { /** @noinline */ @@ -352,15 +360,24 @@ export function jsxProdSignatureRunningInDevWithDynamicChildren( const trackActualOwner = __DEV__ && ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit; + let debugStackDEV = false; + if (__DEV__) { + if (trackActualOwner) { + const previousStackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = ownerStackTraceLimit; + debugStackDEV = Error('react-stack-top-frame'); + Error.stackTraceLimit = previousStackTraceLimit; + } else { + debugStackDEV = unknownOwnerDebugStack; + } + } + return jsxDEVImpl( type, config, maybeKey, isStaticChildren, - __DEV__ && - (trackActualOwner - ? Error('react-stack-top-frame') - : unknownOwnerDebugStack), + debugStackDEV, __DEV__ && (trackActualOwner ? createTask(getTaskName(type)) @@ -379,15 +396,23 @@ export function jsxProdSignatureRunningInDevWithStaticChildren( const trackActualOwner = __DEV__ && ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit; + let debugStackDEV = false; + if (__DEV__) { + if (trackActualOwner) { + const previousStackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = ownerStackTraceLimit; + debugStackDEV = Error('react-stack-top-frame'); + Error.stackTraceLimit = previousStackTraceLimit; + } else { + debugStackDEV = unknownOwnerDebugStack; + } + } return jsxDEVImpl( type, config, maybeKey, isStaticChildren, - __DEV__ && - (trackActualOwner - ? Error('react-stack-top-frame') - : unknownOwnerDebugStack), + debugStackDEV, __DEV__ && (trackActualOwner ? createTask(getTaskName(type)) @@ -408,15 +433,23 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren) { const trackActualOwner = __DEV__ && ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit; + let debugStackDEV = false; + if (__DEV__) { + if (trackActualOwner) { + const previousStackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = ownerStackTraceLimit; + debugStackDEV = Error('react-stack-top-frame'); + Error.stackTraceLimit = previousStackTraceLimit; + } else { + debugStackDEV = unknownOwnerDebugStack; + } + } return jsxDEVImpl( type, config, maybeKey, isStaticChildren, - __DEV__ && - (trackActualOwner - ? Error('react-stack-top-frame') - : unknownOwnerDebugStack), + debugStackDEV, __DEV__ && (trackActualOwner ? createTask(getTaskName(type)) @@ -667,15 +700,23 @@ export function createElement(type, config, children) { const trackActualOwner = __DEV__ && ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit; + let debugStackDEV = false; + if (__DEV__) { + if (trackActualOwner) { + const previousStackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = ownerStackTraceLimit; + debugStackDEV = Error('react-stack-top-frame'); + Error.stackTraceLimit = previousStackTraceLimit; + } else { + debugStackDEV = unknownOwnerDebugStack; + } + } return ReactElement( type, key, props, getOwner(), - __DEV__ && - (trackActualOwner - ? Error('react-stack-top-frame') - : unknownOwnerDebugStack), + debugStackDEV, __DEV__ && (trackActualOwner ? createTask(getTaskName(type)) From dc1becd8937b5ff4e188fe5e9e5552740ef915e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 16 Oct 2025 12:16:04 -0400 Subject: [PATCH 06/11] [DevTools] Remove steps title from scrubber (#34878) The hover now has a reach tooltip for the "environment" instead. --- .../src/devtools/views/SuspenseTab/SuspenseTimeline.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js index 9b70812134288..4712397632c11 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js @@ -160,9 +160,7 @@ function SuspenseTimelineInput() { onClick={skipForward}> -
+
Date: Thu, 16 Oct 2025 12:16:16 -0400 Subject: [PATCH 07/11] [DevTools] Adjust the rects size by one pixel smaller (#34876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures that the outline of a previous rectangle lines up on the same pixel as the next rectangle so that they appear consecutive. Screenshot 2025-10-16 at 11 35 32 AM I don't love this implementation. There's probably a smarter way. Was trying to avoid adding another element. --- .../src/devtools/views/SuspenseTab/SuspenseRects.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js index 5f07bb61001ee..c2a131916504c 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -36,12 +36,14 @@ function ScaledRect({ rect, visible, suspended, + adjust, ...props }: { className: string, rect: Rect, visible: boolean, suspended: boolean, + adjust?: boolean, ... }): React$Node { const viewBox = useContext(ViewBox); @@ -57,8 +59,9 @@ function ScaledRect({ data-visible={visible} data-suspended={suspended} style={{ - width, - height, + // Shrink one pixel so that the bottom outline will line up with the top outline of the next one. + width: adjust ? 'calc(' + width + ' - 1px)' : width, + height: adjust ? 'calc(' + height + ' - 1px)' : height, top: y, left: x, }} @@ -160,6 +163,7 @@ function SuspenseRects({ className={styles.SuspenseRectsRect} rect={rect} data-highlighted={selected} + adjust={true} onClick={handleClick} onDoubleClick={handleDoubleClick} onPointerOver={handlePointerOver} From ed1351c4fb92f84657a0c1a2af5ccef2484f7bd7 Mon Sep 17 00:00:00 2001 From: "Henry Q. Dineen" Date: Thu, 16 Oct 2025 12:46:55 -0400 Subject: [PATCH 08/11] [compiler] improve zod v3 backwards compat (#34877) ## Summary When upgrading to `babel-plugin-react-compiler@1.0.0` in a project that uses `zod@3` we are running into TypeScript errors like: ``` node_modules/babel-plugin-react-compiler/dist/index.d.ts:435:10 - error TS2694: Namespace '"/REDACTED/node_modules/zod/v3/external"' has no exported member 'core'. 435 }, z.core.$strip>>>; ~~~~ ``` This problem seems to be related to d6eb735938bc67b41ad723206ea395ba4d761139, which introduced zod v3/v4 compatibility. Since `zod` is bundled into the compiler source this does not cause runtime issues and only manifests as TypeScript errors. My proposed solution is this PR is to use zod's [subpath versioning strategy](https://zod.dev/v4/versioning?id=versioning-in-zod-4) which allows you to support v3 and v4 APIs on both major versions. Changes in this PR include: - Updated `zod` import paths to `zod/v4` - Bumped min `zod` version to `^3.25.0` for zod which guarantees the `zod/v4` subpath is available. - Updated `zod-validation-error` import paths to `zod-validation-error/v4` - Bumped min `zod-validation-error ` version to `^3.5.0` - Updated `externals` tsup configuration where appropriate. Once the compiler drops zod v3 support we could optionally remove the `/v4` subpath from the imports. ## How did you test this change? Not totally sure the best way to test. I ran `NODE_ENV=production yarn workspace babel-plugin-react-compiler run build --dts` and diffed the `dist/` folder between my change and `v1.0.0` and it looks correct. We have a `patch-package` patch to workaround this for now and it works as expected. ```diff diff --git a/node_modules/babel-plugin-react-compiler/dist/index.d.ts b/node_modules/babel-plugin-react-compiler/dist/index.d.ts index 81c3f3d..daafc2c 100644 --- a/node_modules/babel-plugin-react-compiler/dist/index.d.ts +++ b/node_modules/babel-plugin-react-compiler/dist/index.d.ts @@ -1,7 +1,7 @@ import * as BabelCore from '@babel/core'; import { NodePath as NodePath$1 } from '@babel/core'; import * as t from '@babel/types'; -import { z } from 'zod'; +import { z } from 'zod/v4'; import { NodePath, Scope } from '@babel/traverse'; interface Result { ``` Co-authored-by: Henry Q. Dineen --- .../babel-plugin-react-compiler/package.json | 4 ++-- .../src/Entrypoint/Options.ts | 4 ++-- .../src/HIR/Environment.ts | 4 ++-- .../babel-plugin-react-compiler/src/HIR/HIR.ts | 2 +- .../src/HIR/TypeSchema.ts | 2 +- .../src/Utils/TestUtils.ts | 2 +- .../eslint-plugin-react-compiler/package.json | 4 ++-- .../eslint-plugin-react-compiler/tsup.config.ts | 9 ++++++++- .../packages/react-compiler-healthcheck/package.json | 4 ++-- .../react-compiler-healthcheck/tsup.config.ts | 2 ++ compiler/packages/react-mcp-server/package.json | 2 +- compiler/packages/react-mcp-server/src/index.ts | 2 +- compiler/packages/snap/package.json | 4 +++- compiler/packages/snap/src/sprout/evaluator.ts | 4 ++-- compiler/yarn.lock | 12 ++++++------ packages/eslint-plugin-react-hooks/package.json | 4 ++-- scripts/rollup/bundles.js | 2 ++ yarn.lock | 10 +++++----- 18 files changed, 45 insertions(+), 32 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/package.json b/compiler/packages/babel-plugin-react-compiler/package.json index 8d3f1c8ae68e2..7647bcab1ca42 100644 --- a/compiler/packages/babel-plugin-react-compiler/package.json +++ b/compiler/packages/babel-plugin-react-compiler/package.json @@ -52,8 +52,8 @@ "react-dom": "0.0.0-experimental-4beb1fd8-20241118", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", - "zod": "^3.22.4 || ^4.0.0", - "zod-validation-error": "^3.0.3 || ^4.0.0" + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" }, "resolutions": { "./**/@babel/parser": "7.7.4", diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index 450972a460710..2a117b46617ec 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -6,7 +6,7 @@ */ import * as t from '@babel/types'; -import {z} from 'zod'; +import {z} from 'zod/v4'; import { CompilerDiagnostic, CompilerError, @@ -20,7 +20,7 @@ import { tryParseExternalFunction, } from '../HIR/Environment'; import {hasOwnProperty} from '../Utils/utils'; -import {fromZodError} from 'zod-validation-error'; +import {fromZodError} from 'zod-validation-error/v4'; import {CompilerPipelineValue} from './Pipeline'; const PanicThresholdOptionsSchema = z.enum([ diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 7889e13c2f864..a31ef1c336ed6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -6,8 +6,8 @@ */ import * as t from '@babel/types'; -import {ZodError, z} from 'zod'; -import {fromZodError} from 'zod-validation-error'; +import {ZodError, z} from 'zod/v4'; +import {fromZodError} from 'zod-validation-error/v4'; import {CompilerError} from '../CompilerError'; import {Logger, ProgramContext} from '../Entrypoint'; import {Err, Ok, Result} from '../Utils/Result'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 4d2d4ed80d69c..41e957a54677f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -16,7 +16,7 @@ import {assertExhaustive} from '../Utils/utils'; import {Environment, ReactFunctionType} from './Environment'; import type {HookKind} from './ObjectShape'; import {Type, makeType} from './Types'; -import {z} from 'zod'; +import {z} from 'zod/v4'; import type {AliasingEffect} from '../Inference/AliasingEffects'; import {isReservedWord} from '../Utils/Keyword'; import {Err, Ok, Result} from '../Utils/Result'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts index 42c7d2d89dce1..eeaaebf7a39dc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -6,7 +6,7 @@ */ import {isValidIdentifier} from '@babel/types'; -import {z} from 'zod'; +import {z} from 'zod/v4'; import {Effect, ValueKind} from '..'; import { EffectSchema, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts index b28879f369ada..e84c1e57aae60 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {fromZodError} from 'zod-validation-error'; +import {fromZodError} from 'zod-validation-error/v4'; import {CompilerError} from '../CompilerError'; import { CompilationMode, diff --git a/compiler/packages/eslint-plugin-react-compiler/package.json b/compiler/packages/eslint-plugin-react-compiler/package.json index 6c95bf495c66d..3dd77d0e8bf39 100644 --- a/compiler/packages/eslint-plugin-react-compiler/package.json +++ b/compiler/packages/eslint-plugin-react-compiler/package.json @@ -15,8 +15,8 @@ "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", - "zod": "^3.22.4 || ^4.0.0", - "zod-validation-error": "^3.0.3 || ^4.0.0" + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" }, "devDependencies": { "@babel/preset-env": "^7.22.4", diff --git a/compiler/packages/eslint-plugin-react-compiler/tsup.config.ts b/compiler/packages/eslint-plugin-react-compiler/tsup.config.ts index 4b4f526439461..716c2ffff35e9 100644 --- a/compiler/packages/eslint-plugin-react-compiler/tsup.config.ts +++ b/compiler/packages/eslint-plugin-react-compiler/tsup.config.ts @@ -10,7 +10,14 @@ import {defineConfig} from 'tsup'; export default defineConfig({ entry: ['./src/index.ts'], outDir: './dist', - external: ['@babel/core', 'hermes-parser', 'zod', 'zod-validation-error'], + external: [ + '@babel/core', + 'hermes-parser', + 'zod', + 'zod/v4', + 'zod-validation-error', + 'zod-validation-error/v4', + ], splitting: false, sourcemap: false, dts: false, diff --git a/compiler/packages/react-compiler-healthcheck/package.json b/compiler/packages/react-compiler-healthcheck/package.json index 61825b73d8320..5c3d2f412d97e 100644 --- a/compiler/packages/react-compiler-healthcheck/package.json +++ b/compiler/packages/react-compiler-healthcheck/package.json @@ -17,8 +17,8 @@ "fast-glob": "^3.3.2", "ora": "5.4.1", "yargs": "^17.7.2", - "zod": "^3.22.4 || ^4.0.0", - "zod-validation-error": "^3.0.3 || ^4.0.0" + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" }, "devDependencies": {}, "engines": { diff --git a/compiler/packages/react-compiler-healthcheck/tsup.config.ts b/compiler/packages/react-compiler-healthcheck/tsup.config.ts index 7addc79bf909a..7d2c738dc71a6 100644 --- a/compiler/packages/react-compiler-healthcheck/tsup.config.ts +++ b/compiler/packages/react-compiler-healthcheck/tsup.config.ts @@ -18,7 +18,9 @@ export default defineConfig({ 'ora', 'yargs', 'zod', + 'zod/v4', 'zod-validation-error', + 'zod-validation-error/v4', ], splitting: false, sourcemap: false, diff --git a/compiler/packages/react-mcp-server/package.json b/compiler/packages/react-mcp-server/package.json index 4d744c1d667fb..07dc378de12e9 100644 --- a/compiler/packages/react-mcp-server/package.json +++ b/compiler/packages/react-mcp-server/package.json @@ -24,7 +24,7 @@ "html-to-text": "^9.0.5", "prettier": "^3.3.3", "puppeteer": "^24.7.2", - "zod": "^3.22.4 || ^4.0.0" + "zod": "^3.25.0 || ^4.0.0" }, "devDependencies": { "@types/html-to-text": "^9.0.4", diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts index 9c47346b3c7f8..e5bd794107dc1 100644 --- a/compiler/packages/react-mcp-server/src/index.ts +++ b/compiler/packages/react-mcp-server/src/index.ts @@ -7,7 +7,7 @@ import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'; import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; -import {z} from 'zod'; +import {z} from 'zod/v4'; import {compile, type PrintedCompilerPipelineValue} from './compiler'; import { CompilerPipelineValue, diff --git a/compiler/packages/snap/package.json b/compiler/packages/snap/package.json index 60530f01dd418..085422ab83034 100644 --- a/compiler/packages/snap/package.json +++ b/compiler/packages/snap/package.json @@ -37,7 +37,9 @@ "react": "0.0.0-experimental-4beb1fd8-20241118", "react-dom": "0.0.0-experimental-4beb1fd8-20241118", "readline": "^1.3.0", - "yargs": "^17.7.1" + "yargs": "^17.7.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" }, "devDependencies": { "@babel/core": "^7.19.1", diff --git a/compiler/packages/snap/src/sprout/evaluator.ts b/compiler/packages/snap/src/sprout/evaluator.ts index 8af8487d01109..ba44f01b0aa37 100644 --- a/compiler/packages/snap/src/sprout/evaluator.ts +++ b/compiler/packages/snap/src/sprout/evaluator.ts @@ -9,8 +9,8 @@ import {render} from '@testing-library/react'; import {JSDOM} from 'jsdom'; import React, {MutableRefObject} from 'react'; import util from 'util'; -import {z} from 'zod'; -import {fromZodError} from 'zod-validation-error'; +import {z} from 'zod/v4'; +import {fromZodError} from 'zod-validation-error/v4'; import {initFbt, toJSON} from './shared-runtime'; /** diff --git a/compiler/yarn.lock b/compiler/yarn.lock index daafc705fdcda..764200c2ce9de 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -11505,17 +11505,17 @@ zod-to-json-schema@^3.24.1: resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz" integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g== -"zod-validation-error@^3.0.3 || ^4.0.0": +"zod-validation-error@^3.5.0 || ^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918" integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ== -"zod@^3.22.4 || ^4.0.0": - version "4.1.11" - resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5" - integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg== - zod@^3.23.8, zod@^3.24.1: version "3.24.3" resolved "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz" integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg== + +"zod@^3.25.0 || ^4.0.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0" + integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ== diff --git a/packages/eslint-plugin-react-hooks/package.json b/packages/eslint-plugin-react-hooks/package.json index a22448f11c78f..9a8f8ac353ff7 100644 --- a/packages/eslint-plugin-react-hooks/package.json +++ b/packages/eslint-plugin-react-hooks/package.json @@ -42,8 +42,8 @@ "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", - "zod": "^3.22.4 || ^4.0.0", - "zod-validation-error": "^3.0.3 || ^4.0.0" + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" }, "devDependencies": { "@babel/eslint-parser": "^7.11.4", diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index c66b43797f180..0a7b17ec2cc7f 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -1255,7 +1255,9 @@ const bundles = [ '@babel/core', 'hermes-parser', 'zod', + 'zod/v4', 'zod-validation-error', + 'zod-validation-error/v4', 'crypto', 'util', ], diff --git a/yarn.lock b/yarn.lock index 73519b9f695a0..ad8a3f0085cbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18245,12 +18245,12 @@ zip-stream@^2.1.2: compress-commons "^2.1.1" readable-stream "^3.4.0" -"zod-validation-error@^3.0.3 || ^4.0.0": +"zod-validation-error@^3.5.0 || ^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918" integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ== -"zod@^3.22.4 || ^4.0.0": - version "4.1.11" - resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5" - integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg== +"zod@^3.25.0 || ^4.0.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0" + integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ== From 5418d8bdc12ae00859029bcd2abc9eb65e8f4fb4 Mon Sep 17 00:00:00 2001 From: Ricky Date: Thu, 16 Oct 2025 13:40:26 -0400 Subject: [PATCH 09/11] Fix changelog link (#34879) Closes https://github.com/reactjs/react.dev/issues/8081 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21704e3e29f64..85d8f541245e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2) - [``](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children. - [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event). - [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over. -- [React Performance tracks](https://react.dev/reference/developer-tooling/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools +- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panel’s timeline in your browser developer tools ### New React DOM Features From 2381ecc290c588f6366bdcf377529668bb3cc360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Eirinha?= Date: Thu, 16 Oct 2025 19:18:01 +0100 Subject: [PATCH 10/11] [ESLint] Disallow passing effect event down when inlined as a prop (#34820) ## Summary Fixes https://github.com/facebook/react/issues/34793. We are allowing passing down effect events when they are inlined as a prop. ``` ``` This seems like a case that someone not familiar with `useEffectEvent`'s purpose could fall for so this PR introduces logic to disallow its usage. An alternative implementation would be to modify the name and function of `recordAllUseEffectEventFunctions` to record all `useEffectEvent` instances either assigned to a variable or not, but this seems clearer. Or we could also specifically disallow its usage inside JSX. Feel free to suggest any improvements. ## How did you test this change? - Added a new test in `packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js`. All tests pass. --- .../__tests__/ESLintRulesOfHooks-test.js | 19 ++++++++++++++ .../src/rules/RulesOfHooks.ts | 26 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 3e89624c6d3f1..05bdb1e71ed8f 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -1555,6 +1555,17 @@ const allTests = { `, errors: [useEffectEventError('onClick', false)], }, + { + code: normalizeIndent` + // Invalid because useEffectEvent is being passed down + function MyComponent({ theme }) { + return { + showNotification(theme); + })} />; + } + `, + errors: [{...useEffectEventError(null, false), line: 4}], + }, { code: normalizeIndent` // This should error even though it shares an identifier name with the below @@ -1726,6 +1737,14 @@ function classError(hook) { } function useEffectEventError(fn, called) { + if (fn === null) { + return { + message: + `React Hook "useEffectEvent" can only be called at the top level of your component.` + + ` It cannot be passed down.`, + }; + } + return { message: `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index ba398d850dedb..4e49e96bfe68c 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -171,7 +171,15 @@ function isUseEffectEventIdentifier(node: Node): boolean { return node.type === 'Identifier' && node.name === 'useEffectEvent'; } -function useEffectEventError(fn: string, called: boolean): string { +function useEffectEventError(fn: string | null, called: boolean): string { + // no function identifier, i.e. it is not assigned to a variable + if (fn === null) { + return ( + `React Hook "useEffectEvent" can only be called at the top level of your component.` + + ` It cannot be passed down.` + ); + } + return ( `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + 'Effects and Effect Events in the same component.' + @@ -772,6 +780,22 @@ const rule = { // comparison later when we exit lastEffect = node; } + + // Specifically disallow because this + // case can't be caught by `recordAllUseEffectEventFunctions` as it isn't assigned to a variable + if ( + isUseEffectEventIdentifier(nodeWithoutNamespace) && + node.parent?.type !== 'VariableDeclarator' && + // like in other hooks, calling useEffectEvent at component's top level without assignment is valid + node.parent?.type !== 'ExpressionStatement' + ) { + const message = useEffectEventError(null, false); + + context.report({ + node, + message, + }); + } }, Identifier(node) { From 0e32da71c7540f50ef6bb4bf084043678daae28b Mon Sep 17 00:00:00 2001 From: Damjan Petrovic <146435419+pet210702@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:20:21 +0200 Subject: [PATCH 11/11] Add MIT license header to feature flag utility script (#34833) Added the standard Meta Platforms, Inc. MIT license notice to the top of the feature flag comparison script to ensure compliance with repository licensing requirements and for code consistency. **No functional or logic changes were made to the code.** --- scripts/flags/flags.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/flags/flags.js b/scripts/flags/flags.js index 1130ce80bb073..a02b4d84b341d 100644 --- a/scripts/flags/flags.js +++ b/scripts/flags/flags.js @@ -1,3 +1,9 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ 'use strict'; const babel = require('@babel/register');