Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.Suspense fallback={null} name="outer">
<Component key="one" promise={promise} />
<Component key="two" promise={promise} />
<React.Suspense fallback={null} name="inner">
<Component key="three" promise={promise} />
</React.Suspense>
</React.Suspense>,
);
});

expect(store).toMatchInlineSnapshot(`
[root]
<Suspense name="outer">
[suspense-root] rects={null}
<Suspense name="outer" uniqueSuspenders={true} rects={null}>
`);

await actAsync(() => {
resolve('Hello, World!');
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense name="outer">
<Component key="one">
<Component key="two">
▾ <Suspense name="inner">
<Component key="three">
[suspense-root] rects={null}
<Suspense name="outer" uniqueSuspenders={true} rects={null}>
<Suspense name="inner" uniqueSuspenders={false} 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(
<React.Suspense fallback={null} name="outer">
<Component key="one" promise={promise} />
<React.Suspense fallback={null} name="inner">
<Component key="three" promise={promise} />
</React.Suspense>
</React.Suspense>,
);
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense name="outer">
<Component key="one">
▾ <Suspense name="inner">
<Component key="three">
[suspense-root] rects={null}
<Suspense name="outer" uniqueSuspenders={true} rects={null}>
<Suspense name="inner" uniqueSuspenders={false} 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(
<React.Suspense fallback={null} name="outer">
<React.Suspense fallback={null} name="inner">
<Component key="three" promise={promise} />
</React.Suspense>
</React.Suspense>,
);
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense name="outer">
▾ <Suspense name="inner">
<Component key="three">
[suspense-root] rects={null}
<Suspense name="outer" uniqueSuspenders={true} rects={null}>
<Suspense name="inner" uniqueSuspenders={true} rects={null}>
`);
});

// @reactVersion >= 19
it('cleans up host hoistables', async () => {
function Left() {
Expand Down
40 changes: 27 additions & 13 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
Expand Down Expand Up @@ -6859,6 +6860,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;

Expand Down Expand Up @@ -7815,8 +7818,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;
}
}
Expand Down Expand Up @@ -7893,8 +7901,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;
}
}
Expand Down Expand Up @@ -7964,6 +7977,7 @@ export function attach(
case 'state':
switch (fiber.tag) {
case ClassComponent:
case IncompleteClassComponent:
setInObject(instance.state, path, value);
instance.forceUpdate();
break;
Expand Down
Loading