[DevTools] Fix instrumentation error when reconciling promise-as-a-child (#34587)

This commit is contained in:
Sebastian "Sebbie" Silbermann 2025-09-24 22:50:12 +02:00 committed by GitHub
parent 8ad773b1f3
commit c44fbf43b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 55 additions and 7 deletions

View File

@ -3107,4 +3107,45 @@ describe('Store', () => {
await actAsync(() => render(<span />));
expect(store).toMatchInlineSnapshot(`[root]`);
});
// @reactVersion >= 19.0
it('should reconcile promise-as-a-child', async () => {
function Component({children}) {
return <div>{children}</div>;
}
await actAsync(() =>
render(
<React.Suspense>
{Promise.resolve(<Component key="A">A</Component>)}
</React.Suspense>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Suspense>
<Component key="A">
[suspense-root] rects={[{x:1,y:2,width:1,height:1}]}
<Suspense name="Unknown" rects={[{x:1,y:2,width:1,height:1}]}>
`);
await actAsync(() =>
render(
<React.Suspense>
{Promise.resolve(<Component key="not-A">not A</Component>)}
</React.Suspense>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Suspense>
<Component key="not-A">
[suspense-root] rects={[{x:1,y:2,width:5,height:1}]}
<Suspense name="Unknown" rects={[{x:1,y:2,width:5,height:1}]}>
`);
await actAsync(() => render(null));
expect(store).toMatchInlineSnapshot(``);
});
});

View File

@ -2886,9 +2886,16 @@ export function attach(
previousSuspendedBy: null | Array<ReactAsyncInfo>,
parentSuspenseNode: null | SuspenseNode,
): void {
// Remove any async info from the parent, if they were in the previous set but
// Remove any async info if they were in the previous set but
// is no longer in the new set.
if (previousSuspendedBy !== null && parentSuspenseNode !== null) {
// If we just reconciled a SuspenseNode, we need to remove from that node instead of the parent.
// This is different from inserting because inserting is done during reconiliation
// whereas removal is done after we're done reconciling.
const suspenseNode =
instance.suspenseNode === null
? parentSuspenseNode
: instance.suspenseNode;
if (previousSuspendedBy !== null && suspenseNode !== null) {
const nextSuspendedBy = instance.suspendedBy;
for (let i = 0; i < previousSuspendedBy.length; i++) {
const asyncInfo = previousSuspendedBy[i];
@ -2901,7 +2908,7 @@ export function attach(
// This IO entry is no longer blocking the current tree.
// Let's remove it from the parent SuspenseNode.
const ioInfo = asyncInfo.awaited;
const suspendedBySet = parentSuspenseNode.suspendedBy.get(ioInfo);
const suspendedBySet = suspenseNode.suspendedBy.get(ioInfo);
if (
suspendedBySet === undefined ||
@ -2928,16 +2935,16 @@ export function attach(
}
}
if (suspendedBySet !== undefined && suspendedBySet.size === 0) {
parentSuspenseNode.suspendedBy.delete(asyncInfo.awaited);
suspenseNode.suspendedBy.delete(asyncInfo.awaited);
}
if (
parentSuspenseNode.hasUniqueSuspenders &&
!ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo)
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(parentSuspenseNode, ioInfo);
unblockSuspendedBy(suspenseNode, ioInfo);
}
}
}