@samitouri / QOS-React / commits / ba6590dd7c

[DevTools] Rerender boundaries when they unsuspend when advancing the timeline (#34359)

Sebastian "Sebbie" Silbermann committed Sep 4, 2025 at 10:49 UTC ba6590dd7c18bb01aa9f4187e56aa0de798218f6
3 files changed +51 -18
packages/react-devtools-shared/src/__tests__/store-test.js
+28
@@ -987,6 +987,34 @@ describe('Store', () => {
987 <Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
988 <Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
989 `);
990 +
991 + await actAsync(() => {
992 + agent.overrideSuspenseMilestone({
993 + rendererID,
994 + rootID,
995 + suspendedSet: [],
996 + });
997 + });
998 +
999 + expect(store).toMatchInlineSnapshot(`
1000 + [root]
1001 + ▾ <App>
1002 + <Component key="Outside">
1003 + ▾ <Suspense name="parent">
1004 + <Component key="Unrelated at Start">
1005 + ▾ <Suspense name="one">
1006 + <Component key="Suspense 1 Content">
1007 + ▾ <Suspense name="two">
1008 + <Component key="Suspense 2 Content">
1009 + ▾ <Suspense name="three">
1010 + <Component key="Suspense 3 Content">
1011 + <Component key="Unrelated at End">
1012 + [shell]
1013 + <Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
1014 + <Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}>
1015 + <Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
1016 + <Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
1017 + `);
1018 });
1019
1020 it('should display a partially rendered SuspenseList', async () => {
packages/react-devtools-shared/src/backend/fiber/renderer.js
+3
@@ -7519,6 +7519,9 @@ export function attach(
7519 }
7520
7521 // TODO: Allow overriding the timeline for the specified root.
7522 + forceFallbackForFibers.forEach(fiber => {
7523 + scheduleUpdate(fiber);
7524 + });
7525 forceFallbackForFibers.clear();
7526
7527 for (let i = 0; i < suspendedSet.length; ++i) {
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js
+20 -18
@@ -126,26 +126,28 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
126 }
127
128 function handleChange(event: SyntheticEvent) {
129 - const pendingValue = +event.currentTarget.value;
130 - for (let i = 0; i < timeline.length; i++) {
131 - const forceFallback = i > pendingValue;
132 - const suspense = timeline[i];
133 - const elementID = suspense.id;
134 - const rendererID = store.getRendererIDForElement(elementID);
135 - if (rendererID === null) {
136 - // TODO: Handle disconnected elements.
137 - console.warn(
138 - `No renderer ID found for element ${elementID} in suspense timeline.`,
139 - );
140 - } else {
141 - bridge.send('overrideSuspense', {
142 - id: elementID,
143 - rendererID,
144 - forceFallback,
145 - });
146 - }
129 + if (rootID === undefined) {
130 + return;
131 + }
132 + const rendererID = store.getRendererIDForElement(rootID);
133 + if (rendererID === null) {
134 + console.error(
135 + `No renderer ID found for root element ${rootID} in suspense timeline.`,
136 + );
137 + return;
138 }
139
140 + const pendingValue = +event.currentTarget.value;
141 + const suspendedSet = timeline
142 + .slice(pendingValue + 1)
143 + .map(suspense => suspense.id);
144 +
145 + bridge.send('overrideSuspenseMilestone', {
146 + rendererID,
147 + rootID,
148 + suspendedSet,
149 + });
150 +
151 const suspense = timeline[pendingValue];
152 const elementID = suspense.id;
153 highlightHostInstance(elementID);