[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);