Repository: facebook/react
Pull request: [DevTools] Rerender boundaries when they unsuspend when advancing the timeline

Stacked on https://github.com/facebook/react/pull/34357

We can't just schedule updates on the suspended Fibers since that won't rerender their parents or siblings that were suspended in a prior milestone but no longer are in the new milestone.

Mostly noticed because the new `overrideSuspenseMilestone` from https://github.com/facebook/react/pull/34251 wasn't wired up.


Changed files:
filename: packages/react-devtools-shared/src/__tests__/store-test.js
patch: @@ -987,6 +987,34 @@ describe('Store', () => {
             <Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
             <Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
       `);
+
+      await actAsync(() => {
+        agent.overrideSuspenseMilestone({
+          rendererID,
+          rootID,
+          suspendedSet: [],
+        });
+      });
+
+      expect(store).toMatchInlineSnapshot(`
+        [root]
+          ▾ <App>
+              <Component key="Outside">
+            ▾ <Suspense name="parent">
+                <Component key="Unrelated at Start">
+              ▾ <Suspense name="one">
+                  <Component key="Suspense 1 Content">
+              ▾ <Suspense name="two">
+                  <Component key="Suspense 2 Content">
+              ▾ <Suspense name="three">
+                  <Component key="Suspense 3 Content">
+                <Component key="Unrelated at End">
+        [shell]
+          <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}]}>
+            <Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}>
+            <Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
+            <Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
+      `);
     });
 
     it('should display a partially rendered SuspenseList', async () => {

filename: packages/react-devtools-shared/src/backend/fiber/renderer.js
patch: @@ -7519,6 +7519,9 @@ export function attach(
     }
 
     // TODO: Allow overriding the timeline for the specified root.
+    forceFallbackForFibers.forEach(fiber => {
+      scheduleUpdate(fiber);
+    });
     forceFallbackForFibers.clear();
 
     for (let i = 0; i < suspendedSet.length; ++i) {

filename: packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js
patch: @@ -126,26 +126,28 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
   }
 
   function handleChange(event: SyntheticEvent) {
-    const pendingValue = +event.currentTarget.value;
-    for (let i = 0; i < timeline.length; i++) {
-      const forceFallback = i > pendingValue;
-      const suspense = timeline[i];
-      const elementID = suspense.id;
-      const rendererID = store.getRendererIDForElement(elementID);
-      if (rendererID === null) {
-        // TODO: Handle disconnected elements.
-        console.warn(
-          `No renderer ID found for element ${elementID} in suspense timeline.`,
-        );
-      } else {
-        bridge.send('overrideSuspense', {
-          id: elementID,
-          rendererID,
-          forceFallback,
-        });
-      }
+    if (rootID === undefined) {
+      return;
+    }
+    const rendererID = store.getRendererIDForElement(rootID);
+    if (rendererID === null) {
+      console.error(
+        `No renderer ID found for root element ${rootID} in suspense timeline.`,
+      );
+      return;
     }
 
+    const pendingValue = +event.currentTarget.value;
+    const suspendedSet = timeline
+      .slice(pendingValue + 1)
+      .map(suspense => suspense.id);
+
+    bridge.send('overrideSuspenseMilestone', {
+      rendererID,
+      rootID,
+      suspendedSet,
+    });
+
     const suspense = timeline[pendingValue];
     const elementID = suspense.id;
     highlightHostInstance(elementID);