@samitouri / QOS-React-2 / commits / 5aec1b2a8d

[DevTools] Attach async info in filtered fallback to parent of Suspense (#35456)

Sebastian "Sebbie" Silbermann committed Jan 10, 2026 at 11:33 UTC 5aec1b2a8d1bcafb43a7ddb64dc37eacad081bea
2 files changed +78 -1
packages/react-devtools-shared/src/__tests__/store-test.js
+53
@@ -3143,6 +3143,59 @@ describe('Store', () => {
3143 expect(store).toMatchInlineSnapshot(``);
3144 });
3145
3146 + // @reactVersion >= 17.0
3147 + it('should track suspended by in filtered fallback', async () => {
3148 + function IgnoreMe({promise}) {
3149 + return readValue(promise);
3150 + }
3151 +
3152 + function Component({promise}) {
3153 + return readValue(promise);
3154 + }
3155 +
3156 + await actAsync(
3157 + async () =>
3158 + (store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]),
3159 + );
3160 +
3161 + let resolveFallback;
3162 + const fallbackPromise = new Promise(resolve => {
3163 + resolveFallback = resolve;
3164 + });
3165 + let resolveContent;
3166 + const contentPromise = new Promise(resolve => {
3167 + resolveContent = resolve;
3168 + });
3169 +
3170 + await actAsync(() =>
3171 + render(
3172 + <React.Suspense
3173 + name="main"
3174 + fallback={<IgnoreMe promise={fallbackPromise} />}>
3175 + <Component promise={contentPromise} />
3176 + </React.Suspense>,
3177 + ),
3178 + );
3179 + expect(store).toMatchInlineSnapshot(``);
3180 +
3181 + await actAsync(() => resolveFallback('loading'));
3182 + expect(store).toMatchInlineSnapshot(`
3183 + [root]
3184 + <Suspense name="main">
3185 + [suspense-root] rects={null}
3186 + <Suspense name="main" rects={null}>
3187 + `);
3188 +
3189 + await actAsync(() => resolveContent('content'));
3190 + expect(store).toMatchInlineSnapshot(`
3191 + [root]
3192 + ▾ <Suspense name="main">
3193 + <Component>
3194 + [suspense-root] rects={null}
3195 + <Suspense name="main" rects={null}>
3196 + `);
3197 + });
3198 +
3199 // @reactVersion >= 19
3200 it('should keep suspended boundaries in the Suspense tree but not hidden Activity', async () => {
3201 const Activity = React.Activity || React.unstable_Activity;
packages/react-devtools-shared/src/backend/fiber/renderer.js
+25 -1
@@ -2992,6 +2992,30 @@ export function attach(
2992 ) {
2993 parentInstance = parentInstance.parent;
2994 }
2995 + if (parentInstance.kind === FIBER_INSTANCE) {
2996 + const fiber = parentInstance.data;
2997 +
2998 + if (
2999 + fiber.tag === SuspenseComponent &&
3000 + parentInstance !== parentSuspenseNode.instance
3001 + ) {
3002 + // We're about to attach async info to a Suspense boundary we're not
3003 + // actually considering the parent Suspense boundary for this async info.
3004 + // We must have not found a suitable Fiber inside the fallback (e.g. due to filtering).
3005 + // Use the parent of this instance instead since we treat async info
3006 + // attached to a Suspense boundary as that async info triggering the
3007 + // fallback of that boundary.
3008 + const parent = parentInstance.parent;
3009 + if (parent === null) {
3010 + // This shouldn't happen. Any <Suspense> would have at least have the
3011 + // host root as the parent which can't have a fallback.
3012 + throw new Error(
3013 + 'Did not find a suitable instance for this async info. This is a bug in React.',
3014 + );
3015 + }
3016 + parentInstance = parent;
3017 + }
3018 + }
3019
3020 const suspenseNodeSuspendedBy = parentSuspenseNode.suspendedBy;
3021 const ioInfo = asyncInfo.awaited;
@@ -5255,9 +5279,9 @@ export function attach(
5279 // It might even result in a bad user experience for e.g. node selection in the Elements panel.
5280 // The easiest fix is to strip out the intermediate Fragment fibers,
5281 // so the Elements panel and Profiler don't need to special case them.
5258 - // Suspense components only have a non-null memoizedState if they're timed-out.
5282 const isLegacySuspense =
5283 nextFiber.tag === SuspenseComponent && OffscreenComponent === -1;
5284 + // Suspense components only have a non-null memoizedState if they're timed-out.
5285 const prevDidTimeout =
5286 isLegacySuspense && prevFiber.memoizedState !== null;
5287 const nextDidTimeOut =