[Flight] response.readRoot() -> use(response) (#25267)
* [Flight] Move from suspensey readRoot() to use(thenable) * Update noop tests These are no longer sync so they need some more significant updating. Some of these tests are written in a non-idiomatic form too which is not great. * Update Relay tests I kept these as sync for now and just assume a sync Promise. * Updated the main tests * Gate tests * We need to cast through any because Thenable doesn't support unknown strings
Sebastian Markbåge committed
Sep 14, 2022 at 20:20 UTC
975b644643a1479367fddac6b5b8dc854303d6a3
13 files changed
+194
-192
fixtures/flight-browser/index.html
+1
-1
@@ -83,7 +83,7 @@
83
}
84
85
function Shell({ data }) {
86
- let model = data.readRoot();
86
+ let model = React.experimental_use(data);
87
return <div>
88
<Suspense fallback="...">
89
<h1>{model.title}</h1>
fixtures/flight/src/index.js
+1
-1
@@ -6,7 +6,7 @@ import ReactServerDOMReader from 'react-server-dom-webpack';
6
let data = ReactServerDOMReader.createFromFetch(fetch('http://localhost:3001'));
7
8
function Content() {
9
- return data.readRoot();
9
+ return React.experimental_use(data);
10
}
11
12
ReactDOM.render(
packages/react-client/src/ReactFlightClient.js
+2
-5
@@ -147,7 +147,6 @@ Chunk.prototype.then = function<T>(
147
export type ResponseBase = {
148
_bundlerConfig: BundlerConfig,
149
_chunks: Map<number, SomeChunk<any>>,
150
- readRoot<T>(): T,
150
...
151
};
152
@@ -177,10 +176,9 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
176
}
177
}
178
180
-function readRoot<T>(): T {
181
- const response: Response = this;
179
+export function getRoot<T>(response: Response): Thenable<T> {
180
const chunk = getChunk(response, 0);
183
- return readChunk(chunk);
181
+ return (chunk: any);
182
}
183
184
function createPendingChunk<T>(response: Response): PendingChunk<T> {
@@ -541,7 +539,6 @@ export function createResponse(bundlerConfig: BundlerConfig): ResponseBase {
539
const response = {
540
_bundlerConfig: bundlerConfig,
541
_chunks: chunks,
544
- readRoot: readRoot,
542
};
543
return response;
544
}
packages/react-client/src/ReactFlightClientStream.js
+1
-1
@@ -137,4 +137,4 @@ export function createResponse(bundlerConfig: BundlerConfig): Response {
137
return response;
138
}
139
140
-export {reportGlobalError, close} from './ReactFlightClient';
140
+export {reportGlobalError, getRoot, close} from './ReactFlightClient';
packages/react-client/src/__tests__/ReactFlight-test.js
+90
-90
@@ -11,6 +11,8 @@
11
'use strict';
12
13
let act;
14
+let use;
15
+let startTransition;
16
let React;
17
let ReactNoop;
18
let ReactNoopFlightServer;
@@ -24,6 +26,8 @@ describe('ReactFlight', () => {
26
jest.resetModules();
27
28
React = require('react');
29
+ startTransition = React.startTransition;
30
+ use = React.experimental_use;
31
ReactNoop = require('react-noop-renderer');
32
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
33
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
@@ -79,7 +83,7 @@ describe('ReactFlight', () => {
83
};
84
}
85
82
- it('can render a server component', () => {
86
+ it('can render a server component', async () => {
87
function Bar({text}) {
88
return text.toUpperCase();
89
}
@@ -95,7 +99,7 @@ describe('ReactFlight', () => {
99
const transport = ReactNoopFlightServer.render({
100
foo: <Foo />,
101
});
98
- const model = ReactNoopFlightClient.read(transport);
102
+ const model = await ReactNoopFlightClient.read(transport);
103
expect(model).toEqual({
104
foo: {
105
bar: (
@@ -109,7 +113,7 @@ describe('ReactFlight', () => {
113
});
114
});
115
112
- it('can render a client component using a module reference and render there', () => {
116
+ it('can render a client component using a module reference and render there', async () => {
117
function UserClient(props) {
118
return (
119
<span>
@@ -129,8 +133,8 @@ describe('ReactFlight', () => {
133
134
const transport = ReactNoopFlightServer.render(model);
135
132
- act(() => {
133
- const rootModel = ReactNoopFlightClient.read(transport);
136
+ await act(async () => {
137
+ const rootModel = await ReactNoopFlightClient.read(transport);
138
const greeting = rootModel.greeting;
139
ReactNoop.render(greeting);
140
});
@@ -166,15 +170,15 @@ describe('ReactFlight', () => {
170
171
const transport = ReactNoopFlightServer.render(<ServerComponent />);
172
169
- act(() => {
170
- const rootModel = ReactNoopFlightClient.read(transport);
173
+ await act(async () => {
174
+ const rootModel = await ReactNoopFlightClient.read(transport);
175
ReactNoop.render(rootModel);
176
});
177
expect(ReactNoop).toMatchRenderedOutput('Loading...');
178
await load();
179
176
- act(() => {
177
- const rootModel = ReactNoopFlightClient.read(transport);
180
+ await act(async () => {
181
+ const rootModel = await ReactNoopFlightClient.read(transport);
182
ReactNoop.render(rootModel);
183
});
184
expect(ReactNoop).toMatchRenderedOutput(
@@ -211,8 +215,8 @@ describe('ReactFlight', () => {
215
216
const transport = ReactNoopFlightServer.render(<ServerComponent />);
217
214
- act(() => {
215
- const rootModel = ReactNoopFlightClient.read(transport);
218
+ await act(async () => {
219
+ const rootModel = await ReactNoopFlightClient.read(transport);
220
ReactNoop.render(rootModel);
221
});
222
expect(ReactNoop).toMatchRenderedOutput('Loading...');
@@ -248,15 +252,15 @@ describe('ReactFlight', () => {
252
253
const transport = ReactNoopFlightServer.render(<ServerComponent />);
254
251
- act(() => {
252
- const rootModel = ReactNoopFlightClient.read(transport);
255
+ await act(async () => {
256
+ const rootModel = await ReactNoopFlightClient.read(transport);
257
ReactNoop.render(rootModel);
258
});
259
expect(ReactNoop).toMatchRenderedOutput('Loading...');
260
await load();
261
258
- act(() => {
259
- const rootModel = ReactNoopFlightClient.read(transport);
262
+ await act(async () => {
263
+ const rootModel = await ReactNoopFlightClient.read(transport);
264
ReactNoop.render(rootModel);
265
});
266
expect(ReactNoop).toMatchRenderedOutput(
@@ -293,8 +297,8 @@ describe('ReactFlight', () => {
297
298
const transport = ReactNoopFlightServer.render(<ServerComponent />);
299
296
- act(() => {
297
- const rootModel = ReactNoopFlightClient.read(transport);
300
+ await act(async () => {
301
+ const rootModel = await ReactNoopFlightClient.read(transport);
302
ReactNoop.render(rootModel);
303
});
304
expect(ReactNoop).toMatchRenderedOutput('Loading...');
@@ -331,21 +335,22 @@ describe('ReactFlight', () => {
335
336
const transport = ReactNoopFlightServer.render(<ServerComponent />);
337
334
- act(() => {
335
- const rootModel = ReactNoopFlightClient.read(transport);
338
+ await act(async () => {
339
+ const rootModel = await ReactNoopFlightClient.read(transport);
340
ReactNoop.render(rootModel);
341
});
342
expect(ReactNoop).toMatchRenderedOutput('Loading...');
343
await load();
344
341
- act(() => {
342
- const rootModel = ReactNoopFlightClient.read(transport);
345
+ await act(async () => {
346
+ const rootModel = await ReactNoopFlightClient.read(transport);
347
ReactNoop.render(rootModel);
348
});
349
expect(ReactNoop).toMatchRenderedOutput(<div>I am client</div>);
350
});
351
348
- it('should error if a non-serializable value is passed to a host component', () => {
352
+ // @gate enableUseHook
353
+ it('should error if a non-serializable value is passed to a host component', async () => {
354
function EventHandlerProp() {
355
return (
356
<div className="foo" onClick={function() {}}>
@@ -375,31 +380,34 @@ describe('ReactFlight', () => {
380
const symbol = ReactNoopFlightServer.render(<SymbolProp />, options);
381
const refs = ReactNoopFlightServer.render(<RefProp />, options);
382
378
- function Client({transport}) {
379
- return ReactNoopFlightClient.read(transport);
383
+ function Client({promise}) {
384
+ return use(promise);
385
}
386
382
- act(() => {
383
- ReactNoop.render(
384
- <>
385
- <ErrorBoundary expectedMessage="Event handlers cannot be passed to client component props.">
386
- <Client transport={event} />
387
- </ErrorBoundary>
388
- <ErrorBoundary expectedMessage="Functions cannot be passed directly to client components because they're not serializable.">
389
- <Client transport={fn} />
390
- </ErrorBoundary>
391
- <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to client components.">
392
- <Client transport={symbol} />
393
- </ErrorBoundary>
394
- <ErrorBoundary expectedMessage="Refs cannot be used in server components, nor passed to client components.">
395
- <Client transport={refs} />
396
- </ErrorBoundary>
397
- </>,
398
- );
387
+ await act(async () => {
388
+ startTransition(() => {
389
+ ReactNoop.render(
390
+ <>
391
+ <ErrorBoundary expectedMessage="Event handlers cannot be passed to client component props.">
392
+ <Client promise={ReactNoopFlightClient.read(event)} />
393
+ </ErrorBoundary>
394
+ <ErrorBoundary expectedMessage="Functions cannot be passed directly to client components because they're not serializable.">
395
+ <Client promise={ReactNoopFlightClient.read(fn)} />
396
+ </ErrorBoundary>
397
+ <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to client components.">
398
+ <Client promise={ReactNoopFlightClient.read(symbol)} />
399
+ </ErrorBoundary>
400
+ <ErrorBoundary expectedMessage="Refs cannot be used in server components, nor passed to client components.">
401
+ <Client promise={ReactNoopFlightClient.read(refs)} />
402
+ </ErrorBoundary>
403
+ </>,
404
+ );
405
+ });
406
});
407
});
408
402
- it('should trigger the inner most error boundary inside a client component', () => {
409
+ // @gate enableUseHook
410
+ it('should trigger the inner most error boundary inside a client component', async () => {
411
function ServerComponent() {
412
throw new Error('This was thrown in the server component.');
413
}
@@ -433,16 +441,18 @@ describe('ReactFlight', () => {
441
},
442
});
443
436
- function Client({transport}) {
437
- return ReactNoopFlightClient.read(transport);
444
+ function Client({promise}) {
445
+ return use(promise);
446
}
447
440
- act(() => {
441
- ReactNoop.render(
442
- <NoErrorExpected>
443
- <Client transport={data} />
444
- </NoErrorExpected>,
445
- );
448
+ await act(async () => {
449
+ startTransition(() => {
450
+ ReactNoop.render(
451
+ <NoErrorExpected>
452
+ <Client promise={ReactNoopFlightClient.read(data)} />
453
+ </NoErrorExpected>,
454
+ );
455
+ });
456
});
457
});
458
@@ -451,9 +461,7 @@ describe('ReactFlight', () => {
461
const transport = ReactNoopFlightServer.render(
462
<input value={new Date()} />,
463
);
454
- act(() => {
455
- ReactNoop.render(ReactNoopFlightClient.read(transport));
456
- });
464
+ ReactNoopFlightClient.read(transport);
465
}).toErrorDev(
466
'Only plain objects can be passed to client components from server components. ',
467
{withoutStack: true},
@@ -463,9 +471,7 @@ describe('ReactFlight', () => {
471
it('should warn in DEV if a special object is passed to a host component', () => {
472
expect(() => {
473
const transport = ReactNoopFlightServer.render(<input value={Math} />);
466
- act(() => {
467
- ReactNoop.render(ReactNoopFlightClient.read(transport));
468
- });
474
+ ReactNoopFlightClient.read(transport);
475
}).toErrorDev(
476
'Only plain objects can be passed to client components from server components. ' +
477
'Built-ins like Math are not supported.',
@@ -475,9 +481,7 @@ describe('ReactFlight', () => {
481
482
it('should NOT warn in DEV for key getters', () => {
483
const transport = ReactNoopFlightServer.render(<div key="a" />);
478
- act(() => {
479
- ReactNoop.render(ReactNoopFlightClient.read(transport));
480
- });
484
+ ReactNoopFlightClient.read(transport);
485
});
486
487
it('should warn in DEV if an object with symbols is passed to a host component', () => {
@@ -485,9 +489,7 @@ describe('ReactFlight', () => {
489
const transport = ReactNoopFlightServer.render(
490
<input value={{[Symbol.iterator]: {}}} />,
491
);
488
- act(() => {
489
- ReactNoop.render(ReactNoopFlightClient.read(transport));
490
- });
492
+ ReactNoopFlightClient.read(transport);
493
}).toErrorDev(
494
'Only plain objects can be passed to client components from server components. ' +
495
'Objects with symbol properties like Symbol.iterator are not supported.',
@@ -503,9 +505,7 @@ describe('ReactFlight', () => {
505
const transport = ReactNoopFlightServer.render(
506
<input value={new Foo()} />,
507
);
506
- act(() => {
507
- ReactNoop.render(ReactNoopFlightClient.read(transport));
508
- });
508
+ ReactNoopFlightClient.read(transport);
509
}).toErrorDev(
510
'Only plain objects can be passed to client components from server components. ',
511
{withoutStack: true},
@@ -518,7 +518,7 @@ describe('ReactFlight', () => {
518
return <div prop={id}>{children}</div>;
519
}
520
521
- it('should support useId', () => {
521
+ it('should support useId', async () => {
522
function App() {
523
return (
524
<>
@@ -529,8 +529,8 @@ describe('ReactFlight', () => {
529
}
530
531
const transport = ReactNoopFlightServer.render(<App />);
532
- act(() => {
533
- ReactNoop.render(ReactNoopFlightClient.read(transport));
532
+ await act(async () => {
533
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
534
});
535
expect(ReactNoop).toMatchRenderedOutput(
536
<>
@@ -540,7 +540,7 @@ describe('ReactFlight', () => {
540
);
541
});
542
543
- it('accepts an identifier prefix that prefixes generated ids', () => {
543
+ it('accepts an identifier prefix that prefixes generated ids', async () => {
544
function App() {
545
return (
546
<>
@@ -553,8 +553,8 @@ describe('ReactFlight', () => {
553
const transport = ReactNoopFlightServer.render(<App />, {
554
identifierPrefix: 'foo',
555
});
556
- act(() => {
557
- ReactNoop.render(ReactNoopFlightClient.read(transport));
556
+ await act(async () => {
557
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
558
});
559
expect(ReactNoop).toMatchRenderedOutput(
560
<>
@@ -591,8 +591,8 @@ describe('ReactFlight', () => {
591
const transport = ReactNoopFlightServer.render(<App />);
592
expect(Scheduler).toHaveYielded([]);
593
594
- act(() => {
595
- ReactNoop.render(ReactNoopFlightClient.read(transport));
594
+ await act(async () => {
595
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
596
});
597
598
expect(Scheduler).toHaveYielded(['ClientDoubler']);
@@ -607,7 +607,7 @@ describe('ReactFlight', () => {
607
608
describe('ServerContext', () => {
609
// @gate enableServerContext
610
- it('supports basic createServerContext usage', () => {
610
+ it('supports basic createServerContext usage', async () => {
611
const ServerContext = React.createServerContext(
612
'ServerContext',
613
'hello from server',
@@ -618,17 +618,17 @@ describe('ReactFlight', () => {
618
}
619
620
const transport = ReactNoopFlightServer.render(<Foo />);
621
- act(() => {
621
+ await act(async () => {
622
ServerContext._currentRenderer = null;
623
ServerContext._currentRenderer2 = null;
624
- ReactNoop.render(ReactNoopFlightClient.read(transport));
624
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
625
});
626
627
expect(ReactNoop).toMatchRenderedOutput(<div>hello from server</div>);
628
});
629
630
// @gate enableServerContext
631
- it('propagates ServerContext providers in flight', () => {
631
+ it('propagates ServerContext providers in flight', async () => {
632
const ServerContext = React.createServerContext(
633
'ServerContext',
634
'default',
@@ -649,10 +649,10 @@ describe('ReactFlight', () => {
649
}
650
651
const transport = ReactNoopFlightServer.render(<Foo />);
652
- act(() => {
652
+ await act(async () => {
653
ServerContext._currentRenderer = null;
654
ServerContext._currentRenderer2 = null;
655
- ReactNoop.render(ReactNoopFlightClient.read(transport));
655
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
656
});
657
658
expect(ReactNoop).toMatchRenderedOutput(<div>hi this is server</div>);
@@ -693,7 +693,7 @@ describe('ReactFlight', () => {
693
});
694
695
// @gate enableServerContext
696
- it('propagates ServerContext and cleansup providers in flight', () => {
696
+ it('propagates ServerContext and cleansup providers in flight', async () => {
697
const ServerContext = React.createServerContext(
698
'ServerContext',
699
'default',
@@ -724,8 +724,8 @@ describe('ReactFlight', () => {
724
}
725
726
const transport = ReactNoopFlightServer.render(<Foo />);
727
- act(() => {
728
- ReactNoop.render(ReactNoopFlightClient.read(transport));
727
+ await act(async () => {
728
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
729
});
730
731
expect(ReactNoop).toMatchRenderedOutput(
@@ -788,10 +788,10 @@ describe('ReactFlight', () => {
788
789
expect(Scheduler).toHaveYielded(['rendered']);
790
791
- act(() => {
791
+ await act(async () => {
792
ServerContext._currentRenderer = null;
793
ServerContext._currentRenderer2 = null;
794
- ReactNoop.render(ReactNoopFlightClient.read(transport));
794
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
795
});
796
797
expect(ReactNoop).toMatchRenderedOutput(<div>hi this is server</div>);
@@ -828,10 +828,10 @@ describe('ReactFlight', () => {
828
829
expect(Scheduler).toHaveYielded([]);
830
831
- act(() => {
831
+ await act(async () => {
832
ServerContext._currentRenderer = null;
833
ServerContext._currentRenderer2 = null;
834
- const flightModel = ReactNoopFlightClient.read(transport);
834
+ const flightModel = await ReactNoopFlightClient.read(transport);
835
ReactNoop.render(flightModel.foo);
836
});
837
@@ -856,8 +856,8 @@ describe('ReactFlight', () => {
856
context: [['ServerContext', 'Override']],
857
});
858
859
- act(() => {
860
- const flightModel = ReactNoopFlightClient.read(transport);
859
+ await act(async () => {
860
+ const flightModel = await ReactNoopFlightClient.read(transport);
861
ReactNoop.render(flightModel);
862
});
863
expect(ReactNoop).toMatchRenderedOutput(<span>Override</span>);
@@ -937,8 +937,8 @@ describe('ReactFlight', () => {
937
act = require('jest-react').act;
938
Scheduler = require('scheduler');
939
940
- act(() => {
941
- const serverModel = ReactNoopFlightClient.read(transport);
940
+ await act(async () => {
941
+ const serverModel = await ReactNoopFlightClient.read(transport);
942
ReactNoop.render(<ClientApp serverModel={serverModel} />);
943
});
944
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+3
-3
@@ -20,7 +20,7 @@ import ReactFlightClient from 'react-client/flight';
20
21
type Source = Array<string>;
22
23
-const {createResponse, processStringChunk, close} = ReactFlightClient({
23
+const {createResponse, processStringChunk, getRoot, close} = ReactFlightClient({
24
supportsBinaryStreams: false,
25
resolveModuleReference(bundlerConfig: null, idx: string) {
26
return idx;
@@ -34,13 +34,13 @@ const {createResponse, processStringChunk, close} = ReactFlightClient({
34
},
35
});
36
37
-function read<T>(source: Source): T {
37
+function read<T>(source: Source): Thenable<T> {
38
const response = createResponse(source, null);
39
for (let i = 0; i < source.length; i++) {
40
processStringChunk(response, source[i], 0);
41
}
42
close(response);
43
- return response.readRoot();
43
+ return getRoot(response);
44
}
45
46
export {read};
packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js
+2
-1
@@ -18,9 +18,10 @@ import {
18
resolveSymbol,
19
resolveError,
20
close,
21
+ getRoot,
22
} from 'react-client/src/ReactFlightClient';
23
23
-export {createResponse, close};
24
+export {createResponse, close, getRoot};
25
26
export function resolveRow(response: Response, chunk: RowEncoding): void {
27
if (chunk[0] === 'J') {
packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js
+11
-2
@@ -31,13 +31,22 @@ describe('ReactFlightDOMRelay', () => {
31
});
32
33
function readThrough(data) {
34
- const response = ReactDOMFlightRelayClient.createResponse(null);
34
+ const response = ReactDOMFlightRelayClient.createResponse();
35
for (let i = 0; i < data.length; i++) {
36
const chunk = data[i];
37
ReactDOMFlightRelayClient.resolveRow(response, chunk);
38
}
39
ReactDOMFlightRelayClient.close(response);
40
- const model = response.readRoot();
40
+ const promise = ReactDOMFlightRelayClient.getRoot(response);
41
+ let model;
42
+ let error;
43
+ promise.then(
44
+ m => (model = m),
45
+ e => (error = e),
46
+ );
47
+ if (error) {
48
+ throw error;
49
+ }
50
return model;
51
}
52
packages/react-server-dom-webpack/src/ReactFlightDOMClient.js
+12
-9
@@ -7,12 +7,15 @@
7
* @flow
8
*/
9
10
+import type {Thenable} from 'shared/ReactTypes.js';
11
+
12
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';
13
14
import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig';
15
16
import {
17
createResponse,
18
+ getRoot,
19
reportGlobalError,
20
processStringChunk,
21
processBinaryChunk,
@@ -49,21 +52,21 @@ function startReadingFromStream(
52
.catch(error);
53
}
54
52
-function createFromReadableStream(
55
+function createFromReadableStream<T>(
56
stream: ReadableStream,
57
options?: Options,
55
-): FlightResponse {
58
+): Thenable<T> {
59
const response: FlightResponse = createResponse(
60
options && options.moduleMap ? options.moduleMap : null,
61
);
62
startReadingFromStream(response, stream);
60
- return response;
63
+ return getRoot(response);
64
}
65
63
-function createFromFetch(
66
+function createFromFetch<T>(
67
promiseForResponse: Promise<Response>,
68
options?: Options,
66
-): FlightResponse {
69
+): Thenable<T> {
70
const response: FlightResponse = createResponse(
71
options && options.moduleMap ? options.moduleMap : null,
72
);
@@ -75,13 +78,13 @@ function createFromFetch(
78
reportGlobalError(response, e);
79
},
80
);
78
- return response;
81
+ return getRoot(response);
82
}
83
81
-function createFromXHR(
84
+function createFromXHR<T>(
85
request: XMLHttpRequest,
86
options?: Options,
84
-): FlightResponse {
87
+): Thenable<T> {
88
const response: FlightResponse = createResponse(
89
options && options.moduleMap ? options.moduleMap : null,
90
);
@@ -103,7 +106,7 @@ function createFromXHR(
106
request.addEventListener('error', error);
107
request.addEventListener('abort', error);
108
request.addEventListener('timeout', error);
106
- return response;
109
+ return getRoot(response);
110
}
111
112
export {createFromXHR, createFromFetch, createFromReadableStream};
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+32
-35
@@ -18,6 +18,7 @@ global.TextDecoder = require('util').TextDecoder;
18
global.setImmediate = cb => cb();
19
20
let act;
21
+let use;
22
let clientExports;
23
let clientModuleError;
24
let webpackMap;
@@ -40,6 +41,7 @@ describe('ReactFlightDOM', () => {
41
42
Stream = require('stream');
43
React = require('react');
44
+ use = React.experimental_use;
45
Suspense = React.Suspense;
46
ReactDOMClient = require('react-dom/client');
47
ReactServerDOMWriter = require('react-server-dom-webpack/writer.node.server');
@@ -80,20 +82,6 @@ describe('ReactFlightDOM', () => {
82
};
83
}
84
83
- async function waitForSuspense(fn) {
84
- while (true) {
85
- try {
86
- return fn();
87
- } catch (promise) {
88
- if (typeof promise.then === 'function') {
89
- await promise;
90
- } else {
91
- throw promise;
92
- }
93
- }
94
- }
95
- }
96
-
85
const theInfinitePromise = new Promise(() => {});
86
function InfiniteSuspend() {
87
throw theInfinitePromise;
@@ -126,19 +114,18 @@ describe('ReactFlightDOM', () => {
114
);
115
pipe(writable);
116
const response = ReactServerDOMReader.createFromReadableStream(readable);
129
- await waitForSuspense(() => {
130
- const model = response.readRoot();
131
- expect(model).toEqual({
132
- html: (
133
- <div>
134
- <span>hello</span>
135
- <span>world</span>
136
- </div>
137
- ),
138
- });
117
+ const model = await response;
118
+ expect(model).toEqual({
119
+ html: (
120
+ <div>
121
+ <span>hello</span>
122
+ <span>world</span>
123
+ </div>
124
+ ),
125
});
126
});
127
128
+ // @gate enableUseHook
129
it('should resolve the root', async () => {
130
// Model
131
function Text({children}) {
@@ -160,7 +147,7 @@ describe('ReactFlightDOM', () => {
147
148
// View
149
function Message({response}) {
163
- return <section>{response.readRoot().html}</section>;
150
+ return <section>{use(response).html}</section>;
151
}
152
function App({response}) {
153
return (
@@ -188,6 +175,7 @@ describe('ReactFlightDOM', () => {
175
);
176
});
177
178
+ // @gate enableUseHook
179
it('should not get confused by $', async () => {
180
// Model
181
function RootModel() {
@@ -196,7 +184,7 @@ describe('ReactFlightDOM', () => {
184
185
// View
186
function Message({response}) {
199
- return <p>{response.readRoot().text}</p>;
187
+ return <p>{use(response).text}</p>;
188
}
189
function App({response}) {
190
return (
@@ -222,6 +210,7 @@ describe('ReactFlightDOM', () => {
210
expect(container.innerHTML).toBe('<p>$1</p>');
211
});
212
213
+ // @gate enableUseHook
214
it('should not get confused by @', async () => {
215
// Model
216
function RootModel() {
@@ -230,7 +219,7 @@ describe('ReactFlightDOM', () => {
219
220
// View
221
function Message({response}) {
233
- return <p>{response.readRoot().text}</p>;
222
+ return <p>{use(response).text}</p>;
223
}
224
function App({response}) {
225
return (
@@ -256,6 +245,7 @@ describe('ReactFlightDOM', () => {
245
expect(container.innerHTML).toBe('<p>@div</p>');
246
});
247
248
+ // @gate enableUseHook
249
it('should unwrap async module references', async () => {
250
const AsyncModule = Promise.resolve(function AsyncModule({text}) {
251
return 'Async: ' + text;
@@ -266,7 +256,7 @@ describe('ReactFlightDOM', () => {
256
});
257
258
function Print({response}) {
269
- return <p>{response.readRoot()}</p>;
259
+ return <p>{use(response)}</p>;
260
}
261
262
function App({response}) {
@@ -296,6 +286,7 @@ describe('ReactFlightDOM', () => {
286
expect(container.innerHTML).toBe('<p>Async: Module</p>');
287
});
288
289
+ // @gate enableUseHook
290
it('should be able to import a name called "then"', async () => {
291
const thenExports = {
292
then: function then() {
@@ -304,7 +295,7 @@ describe('ReactFlightDOM', () => {
295
};
296
297
function Print({response}) {
307
- return <p>{response.readRoot()}</p>;
298
+ return <p>{use(response)}</p>;
299
}
300
301
function App({response}) {
@@ -333,6 +324,7 @@ describe('ReactFlightDOM', () => {
324
expect(container.innerHTML).toBe('<p>and then</p>');
325
});
326
327
+ // @gate enableUseHook
328
it('should progressively reveal server components', async () => {
329
let reportedErrors = [];
330
@@ -432,7 +424,7 @@ describe('ReactFlightDOM', () => {
424
};
425
426
function ProfilePage({response}) {
435
- return response.readRoot().rootContent;
427
+ return use(response).rootContent;
428
}
429
430
const {writable, readable} = getTestStream();
@@ -520,11 +512,12 @@ describe('ReactFlightDOM', () => {
512
expect(reportedErrors).toEqual([]);
513
});
514
515
+ // @gate enableUseHook
516
it('should preserve state of client components on refetch', async () => {
517
// Client
518
519
function Page({response}) {
527
- return response.readRoot();
520
+ return use(response);
521
}
522
523
function Input() {
@@ -605,6 +598,7 @@ describe('ReactFlightDOM', () => {
598
expect(inputB.value).toBe('goodbye');
599
});
600
601
+ // @gate enableUseHook
602
it('should be able to complete after aborting and throw the reason client-side', async () => {
603
const reportedErrors = [];
604
@@ -627,7 +621,7 @@ describe('ReactFlightDOM', () => {
621
const root = ReactDOMClient.createRoot(container);
622
623
function App({res}) {
630
- return res.readRoot();
624
+ return use(res);
625
}
626
627
await act(async () => {
@@ -649,6 +643,7 @@ describe('ReactFlightDOM', () => {
643
expect(reportedErrors).toEqual(['for reasons']);
644
});
645
646
+ // @gate enableUseHook
647
it('should be able to recover from a direct reference erroring client-side', async () => {
648
const reportedErrors = [];
649
@@ -677,7 +672,7 @@ describe('ReactFlightDOM', () => {
672
const root = ReactDOMClient.createRoot(container);
673
674
function App({res}) {
680
- return res.readRoot();
675
+ return use(res);
676
}
677
678
await act(async () => {
@@ -694,6 +689,7 @@ describe('ReactFlightDOM', () => {
689
expect(reportedErrors).toEqual([]);
690
});
691
692
+ // @gate enableUseHook
693
it('should be able to recover from a direct reference erroring client-side async', async () => {
694
const reportedErrors = [];
695
@@ -727,7 +723,7 @@ describe('ReactFlightDOM', () => {
723
const root = ReactDOMClient.createRoot(container);
724
725
function App({res}) {
730
- return res.readRoot();
726
+ return use(res);
727
}
728
729
await act(async () => {
@@ -751,6 +747,7 @@ describe('ReactFlightDOM', () => {
747
expect(reportedErrors).toEqual([]);
748
});
749
750
+ // @gate enableUseHook
751
it('should be able to recover from a direct reference erroring server-side', async () => {
752
const reportedErrors = [];
753
@@ -787,7 +784,7 @@ describe('ReactFlightDOM', () => {
784
const root = ReactDOMClient.createRoot(container);
785
786
function App({res}) {
790
- return res.readRoot();
787
+ return use(res);
788
}
789
790
await act(async () => {
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+27
-42
@@ -43,20 +43,6 @@ describe('ReactFlightDOMBrowser', () => {
43
use = React.experimental_use;
44
});
45
46
- async function waitForSuspense(fn) {
47
- while (true) {
48
- try {
49
- return fn();
50
- } catch (promise) {
51
- if (typeof promise.then === 'function') {
52
- await promise;
53
- } else {
54
- throw promise;
55
- }
56
- }
57
- }
58
- }
59
-
46
async function readResult(stream) {
47
const reader = stream.getReader();
48
let result = '';
@@ -121,16 +107,14 @@ describe('ReactFlightDOMBrowser', () => {
107
108
const stream = ReactServerDOMWriter.renderToReadableStream(<App />);
109
const response = ReactServerDOMReader.createFromReadableStream(stream);
124
- await waitForSuspense(() => {
125
- const model = response.readRoot();
126
- expect(model).toEqual({
127
- html: (
128
- <div>
129
- <span>hello</span>
130
- <span>world</span>
131
- </div>
132
- ),
133
- });
110
+ const model = await response;
111
+ expect(model).toEqual({
112
+ html: (
113
+ <div>
114
+ <span>hello</span>
115
+ <span>world</span>
116
+ </div>
117
+ ),
118
});
119
});
120
@@ -156,19 +140,18 @@ describe('ReactFlightDOMBrowser', () => {
140
141
const stream = ReactServerDOMWriter.renderToReadableStream(<App />);
142
const response = ReactServerDOMReader.createFromReadableStream(stream);
159
- await waitForSuspense(() => {
160
- const model = response.readRoot();
161
- expect(model).toEqual({
162
- html: (
163
- <div>
164
- <span>hello</span>
165
- <span>world</span>
166
- </div>
167
- ),
168
- });
143
+ const model = await response;
144
+ expect(model).toEqual({
145
+ html: (
146
+ <div>
147
+ <span>hello</span>
148
+ <span>world</span>
149
+ </div>
150
+ ),
151
});
152
});
153
154
+ // @gate enableUseHook
155
it('should progressively reveal server components', async () => {
156
let reportedErrors = [];
157
@@ -259,7 +242,7 @@ describe('ReactFlightDOMBrowser', () => {
242
};
243
244
function ProfilePage({response}) {
262
- return response.readRoot().rootContent;
245
+ return use(response).rootContent;
246
}
247
248
const stream = ReactServerDOMWriter.renderToReadableStream(
@@ -456,6 +439,7 @@ describe('ReactFlightDOMBrowser', () => {
439
expect(isDone).toBeTruthy();
440
});
441
442
+ // @gate enableUseHook
443
it('should allow an alternative module mapping to be used for SSR', async () => {
444
function ClientComponent() {
445
return <span>Client Component</span>;
@@ -491,7 +475,7 @@ describe('ReactFlightDOMBrowser', () => {
475
});
476
477
function ClientRoot() {
494
- return response.readRoot();
478
+ return use(response);
479
}
480
481
const ssrStream = await ReactDOMServer.renderToReadableStream(
@@ -501,6 +485,7 @@ describe('ReactFlightDOMBrowser', () => {
485
expect(result).toEqual('<span>Client Component</span>');
486
});
487
488
+ // @gate enableUseHook
489
it('should be able to complete after aborting and throw the reason client-side', async () => {
490
const reportedErrors = [];
491
@@ -539,7 +524,7 @@ describe('ReactFlightDOMBrowser', () => {
524
const root = ReactDOMClient.createRoot(container);
525
526
function App({res}) {
542
- return res.readRoot();
527
+ return use(res);
528
}
529
530
await act(async () => {
@@ -579,7 +564,7 @@ describe('ReactFlightDOMBrowser', () => {
564
const response = ReactServerDOMReader.createFromReadableStream(stream);
565
566
function Client() {
582
- return response.readRoot();
567
+ return use(response);
568
}
569
570
const container = document.createElement('div');
@@ -613,7 +598,7 @@ describe('ReactFlightDOMBrowser', () => {
598
const response = ReactServerDOMReader.createFromReadableStream(stream);
599
600
function Client() {
616
- return response.readRoot();
601
+ return use(response);
602
}
603
604
const container = document.createElement('div');
@@ -644,7 +629,7 @@ describe('ReactFlightDOMBrowser', () => {
629
const response = ReactServerDOMReader.createFromReadableStream(stream);
630
631
function Client() {
647
- return response.readRoot();
632
+ return use(response);
633
}
634
635
const container = document.createElement('div');
@@ -699,7 +684,7 @@ describe('ReactFlightDOMBrowser', () => {
684
}
685
686
function Client() {
702
- return response.readRoot();
687
+ return use(response);
688
}
689
690
const container = document.createElement('div');
@@ -733,7 +718,7 @@ describe('ReactFlightDOMBrowser', () => {
718
const response = ReactServerDOMReader.createFromReadableStream(stream);
719
720
function Client() {
736
- return response.readRoot();
721
+ return use(response);
722
}
723
724
const container = document.createElement('div');
packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js
+2
-1
@@ -18,9 +18,10 @@ import {
18
resolveSymbol,
19
resolveError,
20
close,
21
+ getRoot,
22
} from 'react-client/src/ReactFlightClient';
23
23
-export {createResponse, close};
24
+export {createResponse, close, getRoot};
25
26
export function resolveRow(response: Response, chunk: RowEncoding): void {
27
if (chunk[0] === 'J') {
packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js
+10
-1
@@ -48,7 +48,16 @@ describe('ReactFlightNativeRelay', () => {
48
ReactNativeFlightRelayClient.resolveRow(response, chunk);
49
}
50
ReactNativeFlightRelayClient.close(response);
51
- const model = response.readRoot();
51
+ const promise = ReactNativeFlightRelayClient.getRoot(response);
52
+ let model;
53
+ let error;
54
+ promise.then(
55
+ m => (model = m),
56
+ e => (error = e),
57
+ );
58
+ if (error) {
59
+ throw error;
60
+ }
61
return model;
62
}
63