@samitouri / QOS-React-2 / commits / 975b644643

[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