From 70890e7c58abccef35a6498f7ee702d608be79d6 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Tue, 10 Feb 2026 16:35:02 +0100 Subject: [PATCH 1/2] Consistent handling of work tags for rename, delete, and overriding state (#35740) Co-authored-by: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com> --- .../src/backend/fiber/renderer.js | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 916d69823285..efe809608cb9 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -6859,6 +6859,8 @@ export function attach( // TODO Show custom UI for Cache like we do for Suspense // For now, just hide state data entirely since it's not meant to be inspected. + // Make sure delete, rename, and override of state handles all tags for which + // we show state. const showState = tag === ClassComponent || tag === IncompleteClassComponent; @@ -7815,8 +7817,13 @@ export function attach( } break; case 'state': - deletePathInObject(instance.state, path); - instance.forceUpdate(); + switch (fiber.tag) { + case ClassComponent: + case IncompleteClassComponent: + deletePathInObject(instance.state, path); + instance.forceUpdate(); + break; + } break; } } @@ -7893,8 +7900,13 @@ export function attach( } break; case 'state': - renamePathInObject(instance.state, oldPath, newPath); - instance.forceUpdate(); + switch (fiber.tag) { + case ClassComponent: + case IncompleteClassComponent: + renamePathInObject(instance.state, oldPath, newPath); + instance.forceUpdate(); + break; + } break; } } @@ -7964,6 +7976,7 @@ export function attach( case 'state': switch (fiber.tag) { case ClassComponent: + case IncompleteClassComponent: setInObject(instance.state, path, value); instance.forceUpdate(); break; From 57b79b0388b755096216b2b5308113e54eac3be8 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Tue, 10 Feb 2026 17:52:35 +0100 Subject: [PATCH 2/2] [DevTools] Only block child Suspense boundaries if the parent has all shared suspenders removed (#35737) --- .../src/__tests__/store-test.js | 97 +++++++++++++++++++ .../src/backend/fiber/renderer.js | 19 ++-- 2 files changed, 107 insertions(+), 9 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index f3bcf189c8ca..36198ac1e079 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -3617,6 +3617,103 @@ describe('Store', () => { `); }); + // @reactVersion >= 17.0 + it('continues to consider Suspense boundary as blocking if some child still is suspended on removed io', async () => { + function Component({promise}) { + readValue(promise); + return null; + } + + let resolve; + const promise = new Promise(_resolve => { + resolve = _resolve; + }); + + await actAsync(() => { + render( + + + + + + + , + ); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + + [suspense-root] rects={null} + + `); + + await actAsync(() => { + resolve('Hello, World!'); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + + + ▾ + + [suspense-root] rects={null} + + + `); + + // We remove one suspender. + // The inner one shouldn't have unique suspenders because it's still blocked + // by the outer one. + await actAsync(() => { + render( + + + + + + , + ); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + + ▾ + + [suspense-root] rects={null} + + + `); + + // Now we remove all unique suspenders of the outer Suspense boundary. + // The inner one is now independently revealed from the parent and should + // be marked as having unique suspenders. + // TODO: The outer boundary no longer has unique suspenders. + await actAsync(() => { + render( + + + + + , + ); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + + [suspense-root] rects={null} + + + `); + }); + // @reactVersion >= 19 it('cleans up host hoistables', async () => { function Left() { diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index efe809608cb9..9b2022daacbd 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -3197,15 +3197,16 @@ export function attach( environmentCounts.set(env, count - 1); } } - } - if ( - suspenseNode.hasUniqueSuspenders && - !ioExistsInSuspenseAncestor(suspenseNode, ioInfo) - ) { - // This entry wasn't in any ancestor and is no longer in this suspense boundary. - // This means that a child might now be the unique suspender for this IO. - // Search the child boundaries to see if we can reveal any of them. - unblockSuspendedBy(suspenseNode, ioInfo); + + if ( + suspenseNode.hasUniqueSuspenders && + !ioExistsInSuspenseAncestor(suspenseNode, ioInfo) + ) { + // This entry wasn't in any ancestor and is no longer in this suspense boundary. + // This means that a child might now be the unique suspender for this IO. + // Search the child boundaries to see if we can reveal any of them. + unblockSuspendedBy(suspenseNode, ioInfo); + } } } }