Skip to content

Commit

Permalink
[Flight] Aborting with a postpone instance as a reason should postpon…
Browse files Browse the repository at this point in the history
…e remaining holes (#27576)

This lets you abort with postponing semantics.
  • Loading branch information
sebmarkbage authored Oct 24, 2023
1 parent b8e47d9 commit 960ed6e
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1309,4 +1309,62 @@ describe('ReactFlightDOMBrowser', () => {
'The render was aborted by the server without a reason.',
]);
});

// @gate enablePostpone
it('postpones when abort passes a postpone signal', async () => {
const infinitePromise = new Promise(() => {});
function Server() {
return infinitePromise;
}

let postponed = null;
let error = null;

const controller = new AbortController();
const stream = ReactServerDOMServer.renderToReadableStream(
<Suspense fallback="Loading...">
<Server />
</Suspense>,
null,
{
onError(x) {
error = x;
},
onPostpone(reason) {
postponed = reason;
},
signal: controller.signal,
},
);

try {
React.unstable_postpone('testing postpone');
} catch (reason) {
controller.abort(reason);
}

const response = ReactServerDOMClient.createFromReadableStream(stream);

function Client() {
return use(response);
}

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<div>
Shell: <Client />
</div>,
);
});
// We should have reserved the shell already. Which means that the Server
// Component should've been a lazy component.
expect(container.innerHTML).toContain('Shell:');
expect(container.innerHTML).toContain('Loading...');
expect(container.innerHTML).not.toContain('Not shown');

expect(postponed).toBe('testing postpone');
expect(error).toBe(null);
});
});
26 changes: 19 additions & 7 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1790,15 +1790,27 @@ export function abort(request: Request, reason: mixed): void {
if (abortableTasks.size > 0) {
// We have tasks to abort. We'll emit one error row and then emit a reference
// to that row from every row that's still remaining.
const error =
reason === undefined
? new Error('The render was aborted by the server without a reason.')
: reason;

const digest = logRecoverableError(request, error);
request.pendingChunks++;
const errorId = request.nextChunkId++;
emitErrorChunk(request, errorId, digest, error);
if (
enablePostpone &&
typeof reason === 'object' &&
reason !== null &&
(reason: any).$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (reason: any);
logPostpone(request, postponeInstance.message);
emitPostponeChunk(request, errorId, postponeInstance);
} else {
const error =
reason === undefined
? new Error(
'The render was aborted by the server without a reason.',
)
: reason;
const digest = logRecoverableError(request, error);
emitErrorChunk(request, errorId, digest, error);
}
abortableTasks.forEach(task => abortTask(task, request, errorId));
abortableTasks.clear();
}
Expand Down

0 comments on commit 960ed6e

Please sign in to comment.