Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] Expose callbacks in options for when various stages of the content is done #21056

Merged
merged 4 commits into from Mar 23, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 16 additions & 8 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Expand Up @@ -336,7 +336,6 @@ describe('ReactDOMFizzServer', () => {
writable.write(chunk, encoding, next);
};

writable.write('<div id="container-A">');
await act(async () => {
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<Suspense fallback={<Text text="Loading A..." />}>
Expand All @@ -346,13 +345,17 @@ describe('ReactDOMFizzServer', () => {
</div>
</Suspense>,
writableA,
{identifierPrefix: 'A_'},
{
identifierPrefix: 'A_',
onReadyToStream() {
writableA.write('<div id="container-A">');
startWriting();
writableA.write('</div>');
},
},
);
startWriting();
});
writable.write('</div>');

writable.write('<div id="container-B">');
await act(async () => {
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<Suspense fallback={<Text text="Loading B..." />}>
Expand All @@ -362,11 +365,16 @@ describe('ReactDOMFizzServer', () => {
</div>
</Suspense>,
writableB,
{identifierPrefix: 'B_'},
{
identifierPrefix: 'B_',
onReadyToStream() {
writableB.write('<div id="container-B">');
startWriting();
writableB.write('</div>');
},
},
);
startWriting();
});
writable.write('</div>');

expect(getVisibleChildren(container)).toEqual([
<div id="container-A">Loading A...</div>,
Expand Down
59 changes: 59 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
Expand Up @@ -58,12 +58,56 @@ describe('ReactDOMFizzServer', () => {
expect(result).toBe('<div>hello world</div>');
});

// @gate experimental
it('emits all HTML as one unit if we wait until the end to start', async () => {
let hasLoaded = false;
let resolve;
const promise = new Promise(r => (resolve = r));
function Wait() {
if (!hasLoaded) {
throw promise;
}
return 'Done';
}
let isComplete = false;
const stream = ReactDOMFizzServer.renderToReadableStream(
<div>
<Suspense fallback="Loading">
<Wait />
</Suspense>
</div>,
{
onCompleteAll() {
isComplete = true;
},
},
);
await jest.runAllTimers();
expect(isComplete).toBe(false);
// Resolve the loading.
hasLoaded = true;
await resolve();

await jest.runAllTimers();

expect(isComplete).toBe(true);

const result = await readResult(stream);
expect(result).toBe('<div><!--$-->Done<!--/$--></div>');
});

// @gate experimental
it('should error the stream when an error is thrown at the root', async () => {
const reportedErrors = [];
const stream = ReactDOMFizzServer.renderToReadableStream(
<div>
<Throw />
</div>,
{
onError(x) {
reportedErrors.push(x);
},
},
);

let caughtError = null;
Expand All @@ -75,16 +119,23 @@ describe('ReactDOMFizzServer', () => {
}
expect(caughtError).toBe(theError);
expect(result).toBe('');
expect(reportedErrors).toEqual([theError]);
});

// @gate experimental
it('should error the stream when an error is thrown inside a fallback', async () => {
const reportedErrors = [];
const stream = ReactDOMFizzServer.renderToReadableStream(
<div>
<Suspense fallback={<Throw />}>
<InfiniteSuspend />
</Suspense>
</div>,
{
onError(x) {
reportedErrors.push(x);
},
},
);

let caughtError = null;
Expand All @@ -96,20 +147,28 @@ describe('ReactDOMFizzServer', () => {
}
expect(caughtError).toBe(theError);
expect(result).toBe('');
expect(reportedErrors).toEqual([theError]);
});

// @gate experimental
it('should not error the stream when an error is thrown inside suspense boundary', async () => {
const reportedErrors = [];
const stream = ReactDOMFizzServer.renderToReadableStream(
<div>
<Suspense fallback={<div>Loading</div>}>
<Throw />
</Suspense>
</div>,
{
onError(x) {
reportedErrors.push(x);
},
},
);

const result = await readResult(stream);
expect(result).toContain('Loading');
expect(reportedErrors).toEqual([theError]);
});

// @gate experimental
Expand Down
71 changes: 71 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
Expand Up @@ -86,14 +86,68 @@ describe('ReactDOMFizzServer', () => {
);
});

// @gate experimental
it('emits all HTML as one unit if we wait until the end to start', async () => {
let hasLoaded = false;
let resolve;
const promise = new Promise(r => (resolve = r));
function Wait() {
if (!hasLoaded) {
throw promise;
}
return 'Done';
}
let isComplete = false;
const {writable, output} = getTestWritable();
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<div>
<Suspense fallback="Loading">
<Wait />
</Suspense>
</div>,
writable,
{
onCompleteAll() {
isComplete = true;
},
},
);
await jest.runAllTimers();
expect(output.result).toBe('');
expect(isComplete).toBe(false);
// Resolve the loading.
hasLoaded = true;
await resolve();

await jest.runAllTimers();

expect(output.result).toBe('');
expect(isComplete).toBe(true);

// First we write our header.
output.result +=
'<!doctype html><html><head><title>test</title><head><body>';
// Then React starts writing.
startWriting();
expect(output.result).toBe(
'<!doctype html><html><head><title>test</title><head><body><div><!--$-->Done<!--/$--></div>',
);
});

// @gate experimental
it('should error the stream when an error is thrown at the root', async () => {
const reportedErrors = [];
const {writable, output, completed} = getTestWritable();
ReactDOMFizzServer.pipeToNodeWritable(
<div>
<Throw />
</div>,
writable,
{
onError(x) {
reportedErrors.push(x);
},
},
);

// The stream is errored even if we haven't started writing.
Expand All @@ -102,10 +156,13 @@ describe('ReactDOMFizzServer', () => {

expect(output.error).toBe(theError);
expect(output.result).toBe('');
// This type of error is reported to the error callback too.
expect(reportedErrors).toEqual([theError]);
});

// @gate experimental
it('should error the stream when an error is thrown inside a fallback', async () => {
const reportedErrors = [];
const {writable, output, completed} = getTestWritable();
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<div>
Expand All @@ -114,17 +171,24 @@ describe('ReactDOMFizzServer', () => {
</Suspense>
</div>,
writable,
{
onError(x) {
reportedErrors.push(x);
},
},
);
startWriting();

await completed;

expect(output.error).toBe(theError);
expect(output.result).toBe('');
expect(reportedErrors).toEqual([theError]);
});

// @gate experimental
it('should not error the stream when an error is thrown inside suspense boundary', async () => {
const reportedErrors = [];
const {writable, output, completed} = getTestWritable();
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<div>
Expand All @@ -133,13 +197,20 @@ describe('ReactDOMFizzServer', () => {
</Suspense>
</div>,
writable,
{
onError(x) {
reportedErrors.push(x);
},
},
);
startWriting();

await completed;

expect(output.error).toBe(undefined);
expect(output.result).toContain('Loading');
// While no error is reported to the stream, the error is reported to the callback.
expect(reportedErrors).toEqual([theError]);
});

// @gate experimental
Expand Down
17 changes: 15 additions & 2 deletions packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Expand Up @@ -22,6 +22,9 @@ type Options = {
identifierPrefix?: string,
progressiveChunkSize?: number,
signal?: AbortSignal,
onReadyToStream?: () => void,
onCompleteAll?: () => void,
onError?: (error: mixed) => void,
};

function renderToReadableStream(
Expand All @@ -37,21 +40,31 @@ function renderToReadableStream(
};
signal.addEventListener('abort', listener);
}
return new ReadableStream({
const stream = new ReadableStream({
start(controller) {
request = createRequest(
children,
controller,
createResponseState(options ? options.identifierPrefix : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onCompleteAll : undefined,
options ? options.onReadyToStream : undefined,
);
startWork(request);
},
pull(controller) {
startFlowing(request);
// Pull is called immediately even if the stream is not passed to anything.
// That's buffering too early. We want to start buffering once the stream
// is actually used by something so we can give it the best result possible
// at that point.
if (stream.locked) {
startFlowing(request);
}
},
cancel(reason) {},
});
return stream;
}

export {renderToReadableStream};
6 changes: 6 additions & 0 deletions packages/react-dom/src/server/ReactDOMFizzServerNode.js
Expand Up @@ -26,6 +26,9 @@ function createDrainHandler(destination, request) {
type Options = {
identifierPrefix?: string,
progressiveChunkSize?: number,
onReadyToStream?: () => void,
onCompleteAll?: () => void,
onError?: (error: mixed) => void,
};

type Controls = {
Expand All @@ -44,6 +47,9 @@ function pipeToNodeWritable(
destination,
createResponseState(options ? options.identifierPrefix : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onCompleteAll : undefined,
options ? options.onReadyToStream : undefined,
);
let hasStartedFlowing = false;
startWork(request);
Expand Down
6 changes: 6 additions & 0 deletions packages/react-noop-renderer/src/ReactNoopServer.js
Expand Up @@ -217,6 +217,9 @@ const ReactNoopServer = ReactFizzServer({

type Options = {
progressiveChunkSize?: number,
onReadyToStream?: () => void,
onCompleteAll?: () => void,
onError?: (error: mixed) => void,
};

function render(children: React$Element<any>, options?: Options): Destination {
Expand All @@ -234,6 +237,9 @@ function render(children: React$Element<any>, options?: Options): Destination {
destination,
null,
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onCompleteAll : undefined,
options ? options.onReadyToStream : undefined,
);
ReactNoopServer.startWork(request);
ReactNoopServer.startFlowing(request);
Expand Down