Skip to content

Commit

Permalink
experimental_use(promise) for SSR (#25214)
Browse files Browse the repository at this point in the history
Follow up to #25084 and #25207. Implements experimental_use(promise) API
in the SSR runtime (Fizz).

This is largely a copy-paste of the Flight implementation. I have
intentionally tried to keep both as close as possible.
  • Loading branch information
acdlite committed Sep 9, 2022
1 parent d6f9628 commit c28f313
Show file tree
Hide file tree
Showing 6 changed files with 525 additions and 34 deletions.
212 changes: 212 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ let Suspense;
let SuspenseList;
let useSyncExternalStore;
let useSyncExternalStoreWithSelector;
let use;
let PropTypes;
let textCache;
let window;
Expand All @@ -42,6 +43,7 @@ describe('ReactDOMFizzServer', () => {
Suspense = React.Suspense;
if (gate(flags => flags.enableSuspenseList)) {
SuspenseList = React.SuspenseList;
use = React.experimental_use;
}

PropTypes = require('prop-types');
Expand Down Expand Up @@ -5243,5 +5245,215 @@ describe('ReactDOMFizzServer', () => {
console.error = originalConsoleError;
}
});

// @gate enableUseHook
it('basic use(promise)', async () => {
const promiseA = Promise.resolve('A');
const promiseB = Promise.resolve('B');
const promiseC = Promise.resolve('C');

function Async() {
return use(promiseA) + use(promiseB) + use(promiseC);
}

function App() {
return (
<Suspense fallback="Loading...">
<Async />
</Suspense>
);
}

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});

// TODO: The `act` implementation in this file doesn't unwrap microtasks
// automatically. We can't use the same `act` we use for Fiber tests
// because that relies on the mock Scheduler. Doesn't affect any public
// API but we might want to fix this for our own internal tests.
//
// For now, wait for each promise in sequence.
await act(async () => {
await promiseA;
});
await act(async () => {
await promiseB;
});
await act(async () => {
await promiseC;
});

expect(getVisibleChildren(container)).toEqual('ABC');

ReactDOMClient.hydrateRoot(container, <App />);
expect(Scheduler).toFlushAndYield([]);
expect(getVisibleChildren(container)).toEqual('ABC');
});

// @gate enableUseHook
it('use(promise) in multiple components', async () => {
const promiseA = Promise.resolve('A');
const promiseB = Promise.resolve('B');
const promiseC = Promise.resolve('C');
const promiseD = Promise.resolve('D');

function Child({prefix}) {
return prefix + use(promiseC) + use(promiseD);
}

function Parent() {
return <Child prefix={use(promiseA) + use(promiseB)} />;
}

function App() {
return (
<Suspense fallback="Loading...">
<Parent />
</Suspense>
);
}

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});

// TODO: The `act` implementation in this file doesn't unwrap microtasks
// automatically. We can't use the same `act` we use for Fiber tests
// because that relies on the mock Scheduler. Doesn't affect any public
// API but we might want to fix this for our own internal tests.
//
// For now, wait for each promise in sequence.
await act(async () => {
await promiseA;
});
await act(async () => {
await promiseB;
});
await act(async () => {
await promiseC;
});
await act(async () => {
await promiseD;
});

expect(getVisibleChildren(container)).toEqual('ABCD');

ReactDOMClient.hydrateRoot(container, <App />);
expect(Scheduler).toFlushAndYield([]);
expect(getVisibleChildren(container)).toEqual('ABCD');
});

// @gate enableUseHook
it('using a rejected promise will throw', async () => {
const promiseA = Promise.resolve('A');
const promiseB = Promise.reject(new Error('Oops!'));
const promiseC = Promise.resolve('C');

// Jest/Node will raise an unhandled rejected error unless we await this. It
// works fine in the browser, though.
await expect(promiseB).rejects.toThrow('Oops!');

function Async() {
return use(promiseA) + use(promiseB) + use(promiseC);
}

class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
if (this.state.error) {
return this.state.error.message;
}
return this.props.children;
}
}

function App() {
return (
<Suspense fallback="Loading...">
<ErrorBoundary>
<Async />
</ErrorBoundary>
</Suspense>
);
}

const reportedServerErrors = [];
await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
onError(error) {
reportedServerErrors.push(error);
},
});
pipe(writable);
});

// TODO: The `act` implementation in this file doesn't unwrap microtasks
// automatically. We can't use the same `act` we use for Fiber tests
// because that relies on the mock Scheduler. Doesn't affect any public
// API but we might want to fix this for our own internal tests.
//
// For now, wait for each promise in sequence.
await act(async () => {
await promiseA;
});
await act(async () => {
await expect(promiseB).rejects.toThrow('Oops!');
});
await act(async () => {
await promiseC;
});

expect(getVisibleChildren(container)).toEqual('Loading...');
expect(reportedServerErrors.length).toBe(1);
expect(reportedServerErrors[0].message).toBe('Oops!');

const reportedClientErrors = [];
ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
reportedClientErrors.push(error);
},
});
expect(Scheduler).toFlushAndYield([]);
expect(getVisibleChildren(container)).toEqual('Oops!');
expect(reportedClientErrors.length).toBe(1);
if (__DEV__) {
expect(reportedClientErrors[0].message).toBe('Oops!');
} else {
expect(reportedClientErrors[0].message).toBe(
'The server could not finish this Suspense boundary, likely due to ' +
'an error during server rendering. Switched to client rendering.',
);
}
});

// @gate enableUseHook
it("use a promise that's already been instrumented and resolved", async () => {
const thenable = {
status: 'fulfilled',
value: 'Hi',
then() {},
};

// This will never suspend because the thenable already resolved
function App() {
return use(thenable);
}

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
expect(getVisibleChildren(container)).toEqual('Hi');

ReactDOMClient.hydrateRoot(container, <App />);
expect(Scheduler).toFlushAndYield([]);
expect(getVisibleChildren(container)).toEqual('Hi');
});
});
});

0 comments on commit c28f313

Please sign in to comment.