React version: main (d083ec1), 19.3.0-experimental-cbb046ab-20260731
Steps To Reproduce
- Prerender a boundary whose content suspends once, so it gets retried as its own task, and then aborts the prerender synchronously while rendering a later child.
- Resume the postponed state.
Link to code example:
let resolve;
const promise = new Promise(r => (resolve = r));
let prerendering = true;
const controller = new AbortController();
function Abort() {
if (prerendering) {
controller.abort();
}
return 'World';
}
function Content() {
React.use(promise);
return (
<section>
<span>Hello</span>
<Abort />
</section>
);
}
function App() {
return (
<div>
<Suspense fallback="Loading...">
<Content />
</Suspense>
</div>
);
}
const pending = prerender(<App />, {signal: controller.signal});
resolve();
const {postponed} = await pending;
prerendering = false;
const stream = await resume(<App />, JSON.parse(JSON.stringify(postponed)));
The current behavior
The resumed boundary contains only the child that was rendering when the prerender was aborted: <div>World</div>.
retryRenderTask rewinds the segment to where the attempt started, but task.keyPath and task.childIndex still point at <Abort /> when the task is finished through abortUnwoundTask. trackPostpone records the hole as slot 1 of <section>, and the resume renders <Abort /> alone into the boundary's segment.
We run into this in Next.js 16 with cacheComponents: reading Date.now() in a Client Component aborts the prerender synchronously, the page boundary resumes empty and hydration fails with #418.
The expected behavior
The hole covers everything the aborted attempt discarded, so the resume renders the whole content: <div><section><span>Hello</span>World</section></div>.
React version: main (d083ec1), 19.3.0-experimental-cbb046ab-20260731
Steps To Reproduce
Link to code example:
The current behavior
The resumed boundary contains only the child that was rendering when the prerender was aborted:
<div>World</div>.retryRenderTaskrewinds the segment to where the attempt started, buttask.keyPathandtask.childIndexstill point at<Abort />when the task is finished throughabortUnwoundTask.trackPostponerecords the hole as slot 1 of<section>, and the resume renders<Abort />alone into the boundary's segment.We run into this in Next.js 16 with
cacheComponents: readingDate.now()in a Client Component aborts the prerender synchronously, the page boundary resumes empty and hydration fails with #418.The expected behavior
The hole covers everything the aborted attempt discarded, so the resume renders the whole content:
<div><section><span>Hello</span>World</section></div>.