Skip to content

Commit f4a19d4

Browse files
sebmarkbagekoto
authored andcommitted
[Fizz] Expose callbacks in options for when various stages of the content is done (facebook#21056)
* Report errors to a global handler This allows you to log errors or set things like status codes. * Add complete callback * onReadyToStream callback This is typically not needed because if you want to stream when the root is ready you can just start writing immediately. * Rename onComplete -> onCompleteAll
1 parent 391b419 commit f4a19d4

File tree

7 files changed

+256
-61
lines changed

7 files changed

+256
-61
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

+16-8
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,6 @@ describe('ReactDOMFizzServer', () => {
336336
writable.write(chunk, encoding, next);
337337
};
338338

339-
writable.write('<div id="container-A">');
340339
await act(async () => {
341340
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
342341
<Suspense fallback={<Text text="Loading A..." />}>
@@ -346,13 +345,17 @@ describe('ReactDOMFizzServer', () => {
346345
</div>
347346
</Suspense>,
348347
writableA,
349-
{identifierPrefix: 'A_'},
348+
{
349+
identifierPrefix: 'A_',
350+
onReadyToStream() {
351+
writableA.write('<div id="container-A">');
352+
startWriting();
353+
writableA.write('</div>');
354+
},
355+
},
350356
);
351-
startWriting();
352357
});
353-
writable.write('</div>');
354358

355-
writable.write('<div id="container-B">');
356359
await act(async () => {
357360
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
358361
<Suspense fallback={<Text text="Loading B..." />}>
@@ -362,11 +365,16 @@ describe('ReactDOMFizzServer', () => {
362365
</div>
363366
</Suspense>,
364367
writableB,
365-
{identifierPrefix: 'B_'},
368+
{
369+
identifierPrefix: 'B_',
370+
onReadyToStream() {
371+
writableB.write('<div id="container-B">');
372+
startWriting();
373+
writableB.write('</div>');
374+
},
375+
},
366376
);
367-
startWriting();
368377
});
369-
writable.write('</div>');
370378

371379
expect(getVisibleChildren(container)).toEqual([
372380
<div id="container-A">Loading A...</div>,

packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js

+59
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,56 @@ describe('ReactDOMFizzServer', () => {
5858
expect(result).toBe('<div>hello world</div>');
5959
});
6060

61+
// @gate experimental
62+
it('emits all HTML as one unit if we wait until the end to start', async () => {
63+
let hasLoaded = false;
64+
let resolve;
65+
const promise = new Promise(r => (resolve = r));
66+
function Wait() {
67+
if (!hasLoaded) {
68+
throw promise;
69+
}
70+
return 'Done';
71+
}
72+
let isComplete = false;
73+
const stream = ReactDOMFizzServer.renderToReadableStream(
74+
<div>
75+
<Suspense fallback="Loading">
76+
<Wait />
77+
</Suspense>
78+
</div>,
79+
{
80+
onCompleteAll() {
81+
isComplete = true;
82+
},
83+
},
84+
);
85+
await jest.runAllTimers();
86+
expect(isComplete).toBe(false);
87+
// Resolve the loading.
88+
hasLoaded = true;
89+
await resolve();
90+
91+
await jest.runAllTimers();
92+
93+
expect(isComplete).toBe(true);
94+
95+
const result = await readResult(stream);
96+
expect(result).toBe('<div><!--$-->Done<!--/$--></div>');
97+
});
98+
6199
// @gate experimental
62100
it('should error the stream when an error is thrown at the root', async () => {
101+
const reportedErrors = [];
63102
const stream = ReactDOMFizzServer.renderToReadableStream(
64103
<div>
65104
<Throw />
66105
</div>,
106+
{
107+
onError(x) {
108+
reportedErrors.push(x);
109+
},
110+
},
67111
);
68112

69113
let caughtError = null;
@@ -75,16 +119,23 @@ describe('ReactDOMFizzServer', () => {
75119
}
76120
expect(caughtError).toBe(theError);
77121
expect(result).toBe('');
122+
expect(reportedErrors).toEqual([theError]);
78123
});
79124

80125
// @gate experimental
81126
it('should error the stream when an error is thrown inside a fallback', async () => {
127+
const reportedErrors = [];
82128
const stream = ReactDOMFizzServer.renderToReadableStream(
83129
<div>
84130
<Suspense fallback={<Throw />}>
85131
<InfiniteSuspend />
86132
</Suspense>
87133
</div>,
134+
{
135+
onError(x) {
136+
reportedErrors.push(x);
137+
},
138+
},
88139
);
89140

90141
let caughtError = null;
@@ -96,20 +147,28 @@ describe('ReactDOMFizzServer', () => {
96147
}
97148
expect(caughtError).toBe(theError);
98149
expect(result).toBe('');
150+
expect(reportedErrors).toEqual([theError]);
99151
});
100152

101153
// @gate experimental
102154
it('should not error the stream when an error is thrown inside suspense boundary', async () => {
155+
const reportedErrors = [];
103156
const stream = ReactDOMFizzServer.renderToReadableStream(
104157
<div>
105158
<Suspense fallback={<div>Loading</div>}>
106159
<Throw />
107160
</Suspense>
108161
</div>,
162+
{
163+
onError(x) {
164+
reportedErrors.push(x);
165+
},
166+
},
109167
);
110168

111169
const result = await readResult(stream);
112170
expect(result).toContain('Loading');
171+
expect(reportedErrors).toEqual([theError]);
113172
});
114173

115174
// @gate experimental

packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js

+71
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,68 @@ describe('ReactDOMFizzServer', () => {
8686
);
8787
});
8888

89+
// @gate experimental
90+
it('emits all HTML as one unit if we wait until the end to start', async () => {
91+
let hasLoaded = false;
92+
let resolve;
93+
const promise = new Promise(r => (resolve = r));
94+
function Wait() {
95+
if (!hasLoaded) {
96+
throw promise;
97+
}
98+
return 'Done';
99+
}
100+
let isComplete = false;
101+
const {writable, output} = getTestWritable();
102+
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
103+
<div>
104+
<Suspense fallback="Loading">
105+
<Wait />
106+
</Suspense>
107+
</div>,
108+
writable,
109+
{
110+
onCompleteAll() {
111+
isComplete = true;
112+
},
113+
},
114+
);
115+
await jest.runAllTimers();
116+
expect(output.result).toBe('');
117+
expect(isComplete).toBe(false);
118+
// Resolve the loading.
119+
hasLoaded = true;
120+
await resolve();
121+
122+
await jest.runAllTimers();
123+
124+
expect(output.result).toBe('');
125+
expect(isComplete).toBe(true);
126+
127+
// First we write our header.
128+
output.result +=
129+
'<!doctype html><html><head><title>test</title><head><body>';
130+
// Then React starts writing.
131+
startWriting();
132+
expect(output.result).toBe(
133+
'<!doctype html><html><head><title>test</title><head><body><div><!--$-->Done<!--/$--></div>',
134+
);
135+
});
136+
89137
// @gate experimental
90138
it('should error the stream when an error is thrown at the root', async () => {
139+
const reportedErrors = [];
91140
const {writable, output, completed} = getTestWritable();
92141
ReactDOMFizzServer.pipeToNodeWritable(
93142
<div>
94143
<Throw />
95144
</div>,
96145
writable,
146+
{
147+
onError(x) {
148+
reportedErrors.push(x);
149+
},
150+
},
97151
);
98152

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

103157
expect(output.error).toBe(theError);
104158
expect(output.result).toBe('');
159+
// This type of error is reported to the error callback too.
160+
expect(reportedErrors).toEqual([theError]);
105161
});
106162

107163
// @gate experimental
108164
it('should error the stream when an error is thrown inside a fallback', async () => {
165+
const reportedErrors = [];
109166
const {writable, output, completed} = getTestWritable();
110167
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
111168
<div>
@@ -114,17 +171,24 @@ describe('ReactDOMFizzServer', () => {
114171
</Suspense>
115172
</div>,
116173
writable,
174+
{
175+
onError(x) {
176+
reportedErrors.push(x);
177+
},
178+
},
117179
);
118180
startWriting();
119181

120182
await completed;
121183

122184
expect(output.error).toBe(theError);
123185
expect(output.result).toBe('');
186+
expect(reportedErrors).toEqual([theError]);
124187
});
125188

126189
// @gate experimental
127190
it('should not error the stream when an error is thrown inside suspense boundary', async () => {
191+
const reportedErrors = [];
128192
const {writable, output, completed} = getTestWritable();
129193
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
130194
<div>
@@ -133,13 +197,20 @@ describe('ReactDOMFizzServer', () => {
133197
</Suspense>
134198
</div>,
135199
writable,
200+
{
201+
onError(x) {
202+
reportedErrors.push(x);
203+
},
204+
},
136205
);
137206
startWriting();
138207

139208
await completed;
140209

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

145216
// @gate experimental

packages/react-dom/src/server/ReactDOMFizzServerBrowser.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ type Options = {
2222
identifierPrefix?: string,
2323
progressiveChunkSize?: number,
2424
signal?: AbortSignal,
25+
onReadyToStream?: () => void,
26+
onCompleteAll?: () => void,
27+
onError?: (error: mixed) => void,
2528
};
2629

2730
function renderToReadableStream(
@@ -37,21 +40,31 @@ function renderToReadableStream(
3740
};
3841
signal.addEventListener('abort', listener);
3942
}
40-
return new ReadableStream({
43+
const stream = new ReadableStream({
4144
start(controller) {
4245
request = createRequest(
4346
children,
4447
controller,
4548
createResponseState(options ? options.identifierPrefix : undefined),
4649
options ? options.progressiveChunkSize : undefined,
50+
options ? options.onError : undefined,
51+
options ? options.onCompleteAll : undefined,
52+
options ? options.onReadyToStream : undefined,
4753
);
4854
startWork(request);
4955
},
5056
pull(controller) {
51-
startFlowing(request);
57+
// Pull is called immediately even if the stream is not passed to anything.
58+
// That's buffering too early. We want to start buffering once the stream
59+
// is actually used by something so we can give it the best result possible
60+
// at that point.
61+
if (stream.locked) {
62+
startFlowing(request);
63+
}
5264
},
5365
cancel(reason) {},
5466
});
67+
return stream;
5568
}
5669

5770
export {renderToReadableStream};

packages/react-dom/src/server/ReactDOMFizzServerNode.js

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ function createDrainHandler(destination, request) {
2626
type Options = {
2727
identifierPrefix?: string,
2828
progressiveChunkSize?: number,
29+
onReadyToStream?: () => void,
30+
onCompleteAll?: () => void,
31+
onError?: (error: mixed) => void,
2932
};
3033

3134
type Controls = {
@@ -44,6 +47,9 @@ function pipeToNodeWritable(
4447
destination,
4548
createResponseState(options ? options.identifierPrefix : undefined),
4649
options ? options.progressiveChunkSize : undefined,
50+
options ? options.onError : undefined,
51+
options ? options.onCompleteAll : undefined,
52+
options ? options.onReadyToStream : undefined,
4753
);
4854
let hasStartedFlowing = false;
4955
startWork(request);

packages/react-noop-renderer/src/ReactNoopServer.js

+6
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ const ReactNoopServer = ReactFizzServer({
217217

218218
type Options = {
219219
progressiveChunkSize?: number,
220+
onReadyToStream?: () => void,
221+
onCompleteAll?: () => void,
222+
onError?: (error: mixed) => void,
220223
};
221224

222225
function render(children: React$Element<any>, options?: Options): Destination {
@@ -234,6 +237,9 @@ function render(children: React$Element<any>, options?: Options): Destination {
234237
destination,
235238
null,
236239
options ? options.progressiveChunkSize : undefined,
240+
options ? options.onError : undefined,
241+
options ? options.onCompleteAll : undefined,
242+
options ? options.onReadyToStream : undefined,
237243
);
238244
ReactNoopServer.startWork(request);
239245
ReactNoopServer.startFlowing(request);

0 commit comments

Comments
 (0)