diff --git a/fixtures/flight-browser/index.html b/fixtures/flight-browser/index.html index ac39b6e26c6cf..75654b943eb53 100644 --- a/fixtures/flight-browser/index.html +++ b/fixtures/flight-browser/index.html @@ -83,7 +83,7 @@

Flight Example

} function Shell({ data }) { - let model = data.readRoot(); + let model = React.experimental_use(data); return

{model.title}

diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index a096ff25e897b..b54a69ed7ba26 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -6,7 +6,7 @@ import ReactServerDOMReader from 'react-server-dom-webpack'; let data = ReactServerDOMReader.createFromFetch(fetch('http://localhost:3001')); function Content() { - return data.readRoot(); + return React.experimental_use(data); } ReactDOM.render( diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 3ca33ff00e3d3..1dbe46ebce1c8 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -147,7 +147,6 @@ Chunk.prototype.then = function( export type ResponseBase = { _bundlerConfig: BundlerConfig, _chunks: Map>, - readRoot(): T, ... }; @@ -177,10 +176,9 @@ function readChunk(chunk: SomeChunk): T { } } -function readRoot(): T { - const response: Response = this; +export function getRoot(response: Response): Thenable { const chunk = getChunk(response, 0); - return readChunk(chunk); + return (chunk: any); } function createPendingChunk(response: Response): PendingChunk { @@ -541,7 +539,6 @@ export function createResponse(bundlerConfig: BundlerConfig): ResponseBase { const response = { _bundlerConfig: bundlerConfig, _chunks: chunks, - readRoot: readRoot, }; return response; } diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js index d832927a01737..b6c61b0ba5de7 100644 --- a/packages/react-client/src/ReactFlightClientStream.js +++ b/packages/react-client/src/ReactFlightClientStream.js @@ -137,4 +137,4 @@ export function createResponse(bundlerConfig: BundlerConfig): Response { return response; } -export {reportGlobalError, close} from './ReactFlightClient'; +export {reportGlobalError, getRoot, close} from './ReactFlightClient'; diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 505b9072ff7c2..b03a7087656aa 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -11,6 +11,8 @@ 'use strict'; let act; +let use; +let startTransition; let React; let ReactNoop; let ReactNoopFlightServer; @@ -24,6 +26,8 @@ describe('ReactFlight', () => { jest.resetModules(); React = require('react'); + startTransition = React.startTransition; + use = React.experimental_use; ReactNoop = require('react-noop-renderer'); ReactNoopFlightServer = require('react-noop-renderer/flight-server'); ReactNoopFlightClient = require('react-noop-renderer/flight-client'); @@ -79,7 +83,7 @@ describe('ReactFlight', () => { }; } - it('can render a server component', () => { + it('can render a server component', async () => { function Bar({text}) { return text.toUpperCase(); } @@ -95,7 +99,7 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render({ foo: , }); - const model = ReactNoopFlightClient.read(transport); + const model = await ReactNoopFlightClient.read(transport); expect(model).toEqual({ foo: { bar: ( @@ -109,7 +113,7 @@ describe('ReactFlight', () => { }); }); - it('can render a client component using a module reference and render there', () => { + it('can render a client component using a module reference and render there', async () => { function UserClient(props) { return ( @@ -129,8 +133,8 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(model); - act(() => { - const rootModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); const greeting = rootModel.greeting; ReactNoop.render(greeting); }); @@ -166,15 +170,15 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(); - act(() => { - const rootModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(rootModel); }); expect(ReactNoop).toMatchRenderedOutput('Loading...'); await load(); - act(() => { - const rootModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(rootModel); }); expect(ReactNoop).toMatchRenderedOutput( @@ -211,8 +215,8 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(); - act(() => { - const rootModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(rootModel); }); expect(ReactNoop).toMatchRenderedOutput('Loading...'); @@ -248,15 +252,15 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(); - act(() => { - const rootModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(rootModel); }); expect(ReactNoop).toMatchRenderedOutput('Loading...'); await load(); - act(() => { - const rootModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(rootModel); }); expect(ReactNoop).toMatchRenderedOutput( @@ -293,8 +297,8 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(); - act(() => { - const rootModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(rootModel); }); expect(ReactNoop).toMatchRenderedOutput('Loading...'); @@ -331,21 +335,22 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(); - act(() => { - const rootModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(rootModel); }); expect(ReactNoop).toMatchRenderedOutput('Loading...'); await load(); - act(() => { - const rootModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(rootModel); }); expect(ReactNoop).toMatchRenderedOutput(
I am client
); }); - it('should error if a non-serializable value is passed to a host component', () => { + // @gate enableUseHook + it('should error if a non-serializable value is passed to a host component', async () => { function EventHandlerProp() { return (
@@ -375,31 +380,34 @@ describe('ReactFlight', () => { const symbol = ReactNoopFlightServer.render(, options); const refs = ReactNoopFlightServer.render(, options); - function Client({transport}) { - return ReactNoopFlightClient.read(transport); + function Client({promise}) { + return use(promise); } - act(() => { - ReactNoop.render( - <> - - - - - - - - - - - - - , - ); + await act(async () => { + startTransition(() => { + ReactNoop.render( + <> + + + + + + + + + + + + + , + ); + }); }); }); - it('should trigger the inner most error boundary inside a client component', () => { + // @gate enableUseHook + it('should trigger the inner most error boundary inside a client component', async () => { function ServerComponent() { throw new Error('This was thrown in the server component.'); } @@ -433,16 +441,18 @@ describe('ReactFlight', () => { }, }); - function Client({transport}) { - return ReactNoopFlightClient.read(transport); + function Client({promise}) { + return use(promise); } - act(() => { - ReactNoop.render( - - - , - ); + await act(async () => { + startTransition(() => { + ReactNoop.render( + + + , + ); + }); }); }); @@ -451,9 +461,7 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render( , ); - act(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); - }); + ReactNoopFlightClient.read(transport); }).toErrorDev( 'Only plain objects can be passed to client components from server components. ', {withoutStack: true}, @@ -463,9 +471,7 @@ describe('ReactFlight', () => { it('should warn in DEV if a special object is passed to a host component', () => { expect(() => { const transport = ReactNoopFlightServer.render(); - act(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); - }); + ReactNoopFlightClient.read(transport); }).toErrorDev( 'Only plain objects can be passed to client components from server components. ' + 'Built-ins like Math are not supported.', @@ -475,9 +481,7 @@ describe('ReactFlight', () => { it('should NOT warn in DEV for key getters', () => { const transport = ReactNoopFlightServer.render(
); - act(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); - }); + ReactNoopFlightClient.read(transport); }); it('should warn in DEV if an object with symbols is passed to a host component', () => { @@ -485,9 +489,7 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render( , ); - act(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); - }); + ReactNoopFlightClient.read(transport); }).toErrorDev( 'Only plain objects can be passed to client components from server components. ' + 'Objects with symbol properties like Symbol.iterator are not supported.', @@ -503,9 +505,7 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render( , ); - act(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); - }); + ReactNoopFlightClient.read(transport); }).toErrorDev( 'Only plain objects can be passed to client components from server components. ', {withoutStack: true}, @@ -518,7 +518,7 @@ describe('ReactFlight', () => { return
{children}
; } - it('should support useId', () => { + it('should support useId', async () => { function App() { return ( <> @@ -529,8 +529,8 @@ describe('ReactFlight', () => { } const transport = ReactNoopFlightServer.render(); - act(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); }); expect(ReactNoop).toMatchRenderedOutput( <> @@ -540,7 +540,7 @@ describe('ReactFlight', () => { ); }); - it('accepts an identifier prefix that prefixes generated ids', () => { + it('accepts an identifier prefix that prefixes generated ids', async () => { function App() { return ( <> @@ -553,8 +553,8 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(, { identifierPrefix: 'foo', }); - act(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); }); expect(ReactNoop).toMatchRenderedOutput( <> @@ -591,8 +591,8 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(); expect(Scheduler).toHaveYielded([]); - act(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); }); expect(Scheduler).toHaveYielded(['ClientDoubler']); @@ -607,7 +607,7 @@ describe('ReactFlight', () => { describe('ServerContext', () => { // @gate enableServerContext - it('supports basic createServerContext usage', () => { + it('supports basic createServerContext usage', async () => { const ServerContext = React.createServerContext( 'ServerContext', 'hello from server', @@ -618,17 +618,17 @@ describe('ReactFlight', () => { } const transport = ReactNoopFlightServer.render(); - act(() => { + await act(async () => { ServerContext._currentRenderer = null; ServerContext._currentRenderer2 = null; - ReactNoop.render(ReactNoopFlightClient.read(transport)); + ReactNoop.render(await ReactNoopFlightClient.read(transport)); }); expect(ReactNoop).toMatchRenderedOutput(
hello from server
); }); // @gate enableServerContext - it('propagates ServerContext providers in flight', () => { + it('propagates ServerContext providers in flight', async () => { const ServerContext = React.createServerContext( 'ServerContext', 'default', @@ -649,10 +649,10 @@ describe('ReactFlight', () => { } const transport = ReactNoopFlightServer.render(); - act(() => { + await act(async () => { ServerContext._currentRenderer = null; ServerContext._currentRenderer2 = null; - ReactNoop.render(ReactNoopFlightClient.read(transport)); + ReactNoop.render(await ReactNoopFlightClient.read(transport)); }); expect(ReactNoop).toMatchRenderedOutput(
hi this is server
); @@ -693,7 +693,7 @@ describe('ReactFlight', () => { }); // @gate enableServerContext - it('propagates ServerContext and cleansup providers in flight', () => { + it('propagates ServerContext and cleansup providers in flight', async () => { const ServerContext = React.createServerContext( 'ServerContext', 'default', @@ -724,8 +724,8 @@ describe('ReactFlight', () => { } const transport = ReactNoopFlightServer.render(); - act(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); }); expect(ReactNoop).toMatchRenderedOutput( @@ -788,10 +788,10 @@ describe('ReactFlight', () => { expect(Scheduler).toHaveYielded(['rendered']); - act(() => { + await act(async () => { ServerContext._currentRenderer = null; ServerContext._currentRenderer2 = null; - ReactNoop.render(ReactNoopFlightClient.read(transport)); + ReactNoop.render(await ReactNoopFlightClient.read(transport)); }); expect(ReactNoop).toMatchRenderedOutput(
hi this is server
); @@ -828,10 +828,10 @@ describe('ReactFlight', () => { expect(Scheduler).toHaveYielded([]); - act(() => { + await act(async () => { ServerContext._currentRenderer = null; ServerContext._currentRenderer2 = null; - const flightModel = ReactNoopFlightClient.read(transport); + const flightModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(flightModel.foo); }); @@ -856,8 +856,8 @@ describe('ReactFlight', () => { context: [['ServerContext', 'Override']], }); - act(() => { - const flightModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const flightModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(flightModel); }); expect(ReactNoop).toMatchRenderedOutput(Override); @@ -937,8 +937,8 @@ describe('ReactFlight', () => { act = require('jest-react').act; Scheduler = require('scheduler'); - act(() => { - const serverModel = ReactNoopFlightClient.read(transport); + await act(async () => { + const serverModel = await ReactNoopFlightClient.read(transport); ReactNoop.render(); }); diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index 52af83c5ef62a..dd19b702b9ce8 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -20,7 +20,7 @@ import ReactFlightClient from 'react-client/flight'; type Source = Array; -const {createResponse, processStringChunk, close} = ReactFlightClient({ +const {createResponse, processStringChunk, getRoot, close} = ReactFlightClient({ supportsBinaryStreams: false, resolveModuleReference(bundlerConfig: null, idx: string) { return idx; @@ -34,13 +34,13 @@ const {createResponse, processStringChunk, close} = ReactFlightClient({ }, }); -function read(source: Source): T { +function read(source: Source): Thenable { const response = createResponse(source, null); for (let i = 0; i < source.length; i++) { processStringChunk(response, source[i], 0); } close(response); - return response.readRoot(); + return getRoot(response); } export {read}; diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js index ece3124bcf80e..ecee74598c579 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js @@ -18,9 +18,10 @@ import { resolveSymbol, resolveError, close, + getRoot, } from 'react-client/src/ReactFlightClient'; -export {createResponse, close}; +export {createResponse, close, getRoot}; export function resolveRow(response: Response, chunk: RowEncoding): void { if (chunk[0] === 'J') { diff --git a/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js b/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js index 0444add037907..4f762230d9ee9 100644 --- a/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js +++ b/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js @@ -31,13 +31,22 @@ describe('ReactFlightDOMRelay', () => { }); function readThrough(data) { - const response = ReactDOMFlightRelayClient.createResponse(null); + const response = ReactDOMFlightRelayClient.createResponse(); for (let i = 0; i < data.length; i++) { const chunk = data[i]; ReactDOMFlightRelayClient.resolveRow(response, chunk); } ReactDOMFlightRelayClient.close(response); - const model = response.readRoot(); + const promise = ReactDOMFlightRelayClient.getRoot(response); + let model; + let error; + promise.then( + m => (model = m), + e => (error = e), + ); + if (error) { + throw error; + } return model; } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js index 32f8e794f4f70..d2c2dc77a8db2 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js @@ -7,12 +7,15 @@ * @flow */ +import type {Thenable} from 'shared/ReactTypes.js'; + import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream'; import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig'; import { createResponse, + getRoot, reportGlobalError, processStringChunk, processBinaryChunk, @@ -49,21 +52,21 @@ function startReadingFromStream( .catch(error); } -function createFromReadableStream( +function createFromReadableStream( stream: ReadableStream, options?: Options, -): FlightResponse { +): Thenable { const response: FlightResponse = createResponse( options && options.moduleMap ? options.moduleMap : null, ); startReadingFromStream(response, stream); - return response; + return getRoot(response); } -function createFromFetch( +function createFromFetch( promiseForResponse: Promise, options?: Options, -): FlightResponse { +): Thenable { const response: FlightResponse = createResponse( options && options.moduleMap ? options.moduleMap : null, ); @@ -75,13 +78,13 @@ function createFromFetch( reportGlobalError(response, e); }, ); - return response; + return getRoot(response); } -function createFromXHR( +function createFromXHR( request: XMLHttpRequest, options?: Options, -): FlightResponse { +): Thenable { const response: FlightResponse = createResponse( options && options.moduleMap ? options.moduleMap : null, ); @@ -103,7 +106,7 @@ function createFromXHR( request.addEventListener('error', error); request.addEventListener('abort', error); request.addEventListener('timeout', error); - return response; + return getRoot(response); } export {createFromXHR, createFromFetch, createFromReadableStream}; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 9de3843764152..f8948c3604a58 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -18,6 +18,7 @@ global.TextDecoder = require('util').TextDecoder; global.setImmediate = cb => cb(); let act; +let use; let clientExports; let clientModuleError; let webpackMap; @@ -40,6 +41,7 @@ describe('ReactFlightDOM', () => { Stream = require('stream'); React = require('react'); + use = React.experimental_use; Suspense = React.Suspense; ReactDOMClient = require('react-dom/client'); ReactServerDOMWriter = require('react-server-dom-webpack/writer.node.server'); @@ -80,20 +82,6 @@ describe('ReactFlightDOM', () => { }; } - async function waitForSuspense(fn) { - while (true) { - try { - return fn(); - } catch (promise) { - if (typeof promise.then === 'function') { - await promise; - } else { - throw promise; - } - } - } - } - const theInfinitePromise = new Promise(() => {}); function InfiniteSuspend() { throw theInfinitePromise; @@ -126,19 +114,18 @@ describe('ReactFlightDOM', () => { ); pipe(writable); const response = ReactServerDOMReader.createFromReadableStream(readable); - await waitForSuspense(() => { - const model = response.readRoot(); - expect(model).toEqual({ - html: ( -
- hello - world -
- ), - }); + const model = await response; + expect(model).toEqual({ + html: ( +
+ hello + world +
+ ), }); }); + // @gate enableUseHook it('should resolve the root', async () => { // Model function Text({children}) { @@ -160,7 +147,7 @@ describe('ReactFlightDOM', () => { // View function Message({response}) { - return
{response.readRoot().html}
; + return
{use(response).html}
; } function App({response}) { return ( @@ -188,6 +175,7 @@ describe('ReactFlightDOM', () => { ); }); + // @gate enableUseHook it('should not get confused by $', async () => { // Model function RootModel() { @@ -196,7 +184,7 @@ describe('ReactFlightDOM', () => { // View function Message({response}) { - return

{response.readRoot().text}

; + return

{use(response).text}

; } function App({response}) { return ( @@ -222,6 +210,7 @@ describe('ReactFlightDOM', () => { expect(container.innerHTML).toBe('

$1

'); }); + // @gate enableUseHook it('should not get confused by @', async () => { // Model function RootModel() { @@ -230,7 +219,7 @@ describe('ReactFlightDOM', () => { // View function Message({response}) { - return

{response.readRoot().text}

; + return

{use(response).text}

; } function App({response}) { return ( @@ -256,6 +245,7 @@ describe('ReactFlightDOM', () => { expect(container.innerHTML).toBe('

@div

'); }); + // @gate enableUseHook it('should unwrap async module references', async () => { const AsyncModule = Promise.resolve(function AsyncModule({text}) { return 'Async: ' + text; @@ -266,7 +256,7 @@ describe('ReactFlightDOM', () => { }); function Print({response}) { - return

{response.readRoot()}

; + return

{use(response)}

; } function App({response}) { @@ -296,6 +286,7 @@ describe('ReactFlightDOM', () => { expect(container.innerHTML).toBe('

Async: Module

'); }); + // @gate enableUseHook it('should be able to import a name called "then"', async () => { const thenExports = { then: function then() { @@ -304,7 +295,7 @@ describe('ReactFlightDOM', () => { }; function Print({response}) { - return

{response.readRoot()}

; + return

{use(response)}

; } function App({response}) { @@ -333,6 +324,7 @@ describe('ReactFlightDOM', () => { expect(container.innerHTML).toBe('

and then

'); }); + // @gate enableUseHook it('should progressively reveal server components', async () => { let reportedErrors = []; @@ -432,7 +424,7 @@ describe('ReactFlightDOM', () => { }; function ProfilePage({response}) { - return response.readRoot().rootContent; + return use(response).rootContent; } const {writable, readable} = getTestStream(); @@ -520,11 +512,12 @@ describe('ReactFlightDOM', () => { expect(reportedErrors).toEqual([]); }); + // @gate enableUseHook it('should preserve state of client components on refetch', async () => { // Client function Page({response}) { - return response.readRoot(); + return use(response); } function Input() { @@ -605,6 +598,7 @@ describe('ReactFlightDOM', () => { expect(inputB.value).toBe('goodbye'); }); + // @gate enableUseHook it('should be able to complete after aborting and throw the reason client-side', async () => { const reportedErrors = []; @@ -627,7 +621,7 @@ describe('ReactFlightDOM', () => { const root = ReactDOMClient.createRoot(container); function App({res}) { - return res.readRoot(); + return use(res); } await act(async () => { @@ -649,6 +643,7 @@ describe('ReactFlightDOM', () => { expect(reportedErrors).toEqual(['for reasons']); }); + // @gate enableUseHook it('should be able to recover from a direct reference erroring client-side', async () => { const reportedErrors = []; @@ -677,7 +672,7 @@ describe('ReactFlightDOM', () => { const root = ReactDOMClient.createRoot(container); function App({res}) { - return res.readRoot(); + return use(res); } await act(async () => { @@ -694,6 +689,7 @@ describe('ReactFlightDOM', () => { expect(reportedErrors).toEqual([]); }); + // @gate enableUseHook it('should be able to recover from a direct reference erroring client-side async', async () => { const reportedErrors = []; @@ -727,7 +723,7 @@ describe('ReactFlightDOM', () => { const root = ReactDOMClient.createRoot(container); function App({res}) { - return res.readRoot(); + return use(res); } await act(async () => { @@ -751,6 +747,7 @@ describe('ReactFlightDOM', () => { expect(reportedErrors).toEqual([]); }); + // @gate enableUseHook it('should be able to recover from a direct reference erroring server-side', async () => { const reportedErrors = []; @@ -787,7 +784,7 @@ describe('ReactFlightDOM', () => { const root = ReactDOMClient.createRoot(container); function App({res}) { - return res.readRoot(); + return use(res); } await act(async () => { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index f477bba451a6e..7ac0a2b4aa9d1 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -43,20 +43,6 @@ describe('ReactFlightDOMBrowser', () => { use = React.experimental_use; }); - async function waitForSuspense(fn) { - while (true) { - try { - return fn(); - } catch (promise) { - if (typeof promise.then === 'function') { - await promise; - } else { - throw promise; - } - } - } - } - async function readResult(stream) { const reader = stream.getReader(); let result = ''; @@ -121,16 +107,14 @@ describe('ReactFlightDOMBrowser', () => { const stream = ReactServerDOMWriter.renderToReadableStream(); const response = ReactServerDOMReader.createFromReadableStream(stream); - await waitForSuspense(() => { - const model = response.readRoot(); - expect(model).toEqual({ - html: ( -
- hello - world -
- ), - }); + const model = await response; + expect(model).toEqual({ + html: ( +
+ hello + world +
+ ), }); }); @@ -156,19 +140,18 @@ describe('ReactFlightDOMBrowser', () => { const stream = ReactServerDOMWriter.renderToReadableStream(); const response = ReactServerDOMReader.createFromReadableStream(stream); - await waitForSuspense(() => { - const model = response.readRoot(); - expect(model).toEqual({ - html: ( -
- hello - world -
- ), - }); + const model = await response; + expect(model).toEqual({ + html: ( +
+ hello + world +
+ ), }); }); + // @gate enableUseHook it('should progressively reveal server components', async () => { let reportedErrors = []; @@ -259,7 +242,7 @@ describe('ReactFlightDOMBrowser', () => { }; function ProfilePage({response}) { - return response.readRoot().rootContent; + return use(response).rootContent; } const stream = ReactServerDOMWriter.renderToReadableStream( @@ -456,6 +439,7 @@ describe('ReactFlightDOMBrowser', () => { expect(isDone).toBeTruthy(); }); + // @gate enableUseHook it('should allow an alternative module mapping to be used for SSR', async () => { function ClientComponent() { return Client Component; @@ -491,7 +475,7 @@ describe('ReactFlightDOMBrowser', () => { }); function ClientRoot() { - return response.readRoot(); + return use(response); } const ssrStream = await ReactDOMServer.renderToReadableStream( @@ -501,6 +485,7 @@ describe('ReactFlightDOMBrowser', () => { expect(result).toEqual('Client Component'); }); + // @gate enableUseHook it('should be able to complete after aborting and throw the reason client-side', async () => { const reportedErrors = []; @@ -539,7 +524,7 @@ describe('ReactFlightDOMBrowser', () => { const root = ReactDOMClient.createRoot(container); function App({res}) { - return res.readRoot(); + return use(res); } await act(async () => { @@ -579,7 +564,7 @@ describe('ReactFlightDOMBrowser', () => { const response = ReactServerDOMReader.createFromReadableStream(stream); function Client() { - return response.readRoot(); + return use(response); } const container = document.createElement('div'); @@ -613,7 +598,7 @@ describe('ReactFlightDOMBrowser', () => { const response = ReactServerDOMReader.createFromReadableStream(stream); function Client() { - return response.readRoot(); + return use(response); } const container = document.createElement('div'); @@ -644,7 +629,7 @@ describe('ReactFlightDOMBrowser', () => { const response = ReactServerDOMReader.createFromReadableStream(stream); function Client() { - return response.readRoot(); + return use(response); } const container = document.createElement('div'); @@ -699,7 +684,7 @@ describe('ReactFlightDOMBrowser', () => { } function Client() { - return response.readRoot(); + return use(response); } const container = document.createElement('div'); @@ -733,7 +718,7 @@ describe('ReactFlightDOMBrowser', () => { const response = ReactServerDOMReader.createFromReadableStream(stream); function Client() { - return response.readRoot(); + return use(response); } const container = document.createElement('div'); diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js index cea39ac6f2642..087b93479d424 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js @@ -18,9 +18,10 @@ import { resolveSymbol, resolveError, close, + getRoot, } from 'react-client/src/ReactFlightClient'; -export {createResponse, close}; +export {createResponse, close, getRoot}; export function resolveRow(response: Response, chunk: RowEncoding): void { if (chunk[0] === 'J') { diff --git a/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js b/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js index 0d439154b537f..35a2f6684c7cc 100644 --- a/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js +++ b/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js @@ -48,7 +48,16 @@ describe('ReactFlightNativeRelay', () => { ReactNativeFlightRelayClient.resolveRow(response, chunk); } ReactNativeFlightRelayClient.close(response); - const model = response.readRoot(); + const promise = ReactNativeFlightRelayClient.getRoot(response); + let model; + let error; + promise.then( + m => (model = m), + e => (error = e), + ); + if (error) { + throw error; + } return model; }