Skip to content

Commit c28f313

Browse files
authored
experimental_use(promise) for SSR (#25214)
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.
1 parent d6f9628 commit c28f313

File tree

6 files changed

+525
-34
lines changed

6 files changed

+525
-34
lines changed

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

+212
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ let Suspense;
1919
let SuspenseList;
2020
let useSyncExternalStore;
2121
let useSyncExternalStoreWithSelector;
22+
let use;
2223
let PropTypes;
2324
let textCache;
2425
let window;
@@ -42,6 +43,7 @@ describe('ReactDOMFizzServer', () => {
4243
Suspense = React.Suspense;
4344
if (gate(flags => flags.enableSuspenseList)) {
4445
SuspenseList = React.SuspenseList;
46+
use = React.experimental_use;
4547
}
4648

4749
PropTypes = require('prop-types');
@@ -5243,5 +5245,215 @@ describe('ReactDOMFizzServer', () => {
52435245
console.error = originalConsoleError;
52445246
}
52455247
});
5248+
5249+
// @gate enableUseHook
5250+
it('basic use(promise)', async () => {
5251+
const promiseA = Promise.resolve('A');
5252+
const promiseB = Promise.resolve('B');
5253+
const promiseC = Promise.resolve('C');
5254+
5255+
function Async() {
5256+
return use(promiseA) + use(promiseB) + use(promiseC);
5257+
}
5258+
5259+
function App() {
5260+
return (
5261+
<Suspense fallback="Loading...">
5262+
<Async />
5263+
</Suspense>
5264+
);
5265+
}
5266+
5267+
await act(async () => {
5268+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
5269+
pipe(writable);
5270+
});
5271+
5272+
// TODO: The `act` implementation in this file doesn't unwrap microtasks
5273+
// automatically. We can't use the same `act` we use for Fiber tests
5274+
// because that relies on the mock Scheduler. Doesn't affect any public
5275+
// API but we might want to fix this for our own internal tests.
5276+
//
5277+
// For now, wait for each promise in sequence.
5278+
await act(async () => {
5279+
await promiseA;
5280+
});
5281+
await act(async () => {
5282+
await promiseB;
5283+
});
5284+
await act(async () => {
5285+
await promiseC;
5286+
});
5287+
5288+
expect(getVisibleChildren(container)).toEqual('ABC');
5289+
5290+
ReactDOMClient.hydrateRoot(container, <App />);
5291+
expect(Scheduler).toFlushAndYield([]);
5292+
expect(getVisibleChildren(container)).toEqual('ABC');
5293+
});
5294+
5295+
// @gate enableUseHook
5296+
it('use(promise) in multiple components', async () => {
5297+
const promiseA = Promise.resolve('A');
5298+
const promiseB = Promise.resolve('B');
5299+
const promiseC = Promise.resolve('C');
5300+
const promiseD = Promise.resolve('D');
5301+
5302+
function Child({prefix}) {
5303+
return prefix + use(promiseC) + use(promiseD);
5304+
}
5305+
5306+
function Parent() {
5307+
return <Child prefix={use(promiseA) + use(promiseB)} />;
5308+
}
5309+
5310+
function App() {
5311+
return (
5312+
<Suspense fallback="Loading...">
5313+
<Parent />
5314+
</Suspense>
5315+
);
5316+
}
5317+
5318+
await act(async () => {
5319+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
5320+
pipe(writable);
5321+
});
5322+
5323+
// TODO: The `act` implementation in this file doesn't unwrap microtasks
5324+
// automatically. We can't use the same `act` we use for Fiber tests
5325+
// because that relies on the mock Scheduler. Doesn't affect any public
5326+
// API but we might want to fix this for our own internal tests.
5327+
//
5328+
// For now, wait for each promise in sequence.
5329+
await act(async () => {
5330+
await promiseA;
5331+
});
5332+
await act(async () => {
5333+
await promiseB;
5334+
});
5335+
await act(async () => {
5336+
await promiseC;
5337+
});
5338+
await act(async () => {
5339+
await promiseD;
5340+
});
5341+
5342+
expect(getVisibleChildren(container)).toEqual('ABCD');
5343+
5344+
ReactDOMClient.hydrateRoot(container, <App />);
5345+
expect(Scheduler).toFlushAndYield([]);
5346+
expect(getVisibleChildren(container)).toEqual('ABCD');
5347+
});
5348+
5349+
// @gate enableUseHook
5350+
it('using a rejected promise will throw', async () => {
5351+
const promiseA = Promise.resolve('A');
5352+
const promiseB = Promise.reject(new Error('Oops!'));
5353+
const promiseC = Promise.resolve('C');
5354+
5355+
// Jest/Node will raise an unhandled rejected error unless we await this. It
5356+
// works fine in the browser, though.
5357+
await expect(promiseB).rejects.toThrow('Oops!');
5358+
5359+
function Async() {
5360+
return use(promiseA) + use(promiseB) + use(promiseC);
5361+
}
5362+
5363+
class ErrorBoundary extends React.Component {
5364+
state = {error: null};
5365+
static getDerivedStateFromError(error) {
5366+
return {error};
5367+
}
5368+
render() {
5369+
if (this.state.error) {
5370+
return this.state.error.message;
5371+
}
5372+
return this.props.children;
5373+
}
5374+
}
5375+
5376+
function App() {
5377+
return (
5378+
<Suspense fallback="Loading...">
5379+
<ErrorBoundary>
5380+
<Async />
5381+
</ErrorBoundary>
5382+
</Suspense>
5383+
);
5384+
}
5385+
5386+
const reportedServerErrors = [];
5387+
await act(async () => {
5388+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
5389+
onError(error) {
5390+
reportedServerErrors.push(error);
5391+
},
5392+
});
5393+
pipe(writable);
5394+
});
5395+
5396+
// TODO: The `act` implementation in this file doesn't unwrap microtasks
5397+
// automatically. We can't use the same `act` we use for Fiber tests
5398+
// because that relies on the mock Scheduler. Doesn't affect any public
5399+
// API but we might want to fix this for our own internal tests.
5400+
//
5401+
// For now, wait for each promise in sequence.
5402+
await act(async () => {
5403+
await promiseA;
5404+
});
5405+
await act(async () => {
5406+
await expect(promiseB).rejects.toThrow('Oops!');
5407+
});
5408+
await act(async () => {
5409+
await promiseC;
5410+
});
5411+
5412+
expect(getVisibleChildren(container)).toEqual('Loading...');
5413+
expect(reportedServerErrors.length).toBe(1);
5414+
expect(reportedServerErrors[0].message).toBe('Oops!');
5415+
5416+
const reportedClientErrors = [];
5417+
ReactDOMClient.hydrateRoot(container, <App />, {
5418+
onRecoverableError(error) {
5419+
reportedClientErrors.push(error);
5420+
},
5421+
});
5422+
expect(Scheduler).toFlushAndYield([]);
5423+
expect(getVisibleChildren(container)).toEqual('Oops!');
5424+
expect(reportedClientErrors.length).toBe(1);
5425+
if (__DEV__) {
5426+
expect(reportedClientErrors[0].message).toBe('Oops!');
5427+
} else {
5428+
expect(reportedClientErrors[0].message).toBe(
5429+
'The server could not finish this Suspense boundary, likely due to ' +
5430+
'an error during server rendering. Switched to client rendering.',
5431+
);
5432+
}
5433+
});
5434+
5435+
// @gate enableUseHook
5436+
it("use a promise that's already been instrumented and resolved", async () => {
5437+
const thenable = {
5438+
status: 'fulfilled',
5439+
value: 'Hi',
5440+
then() {},
5441+
};
5442+
5443+
// This will never suspend because the thenable already resolved
5444+
function App() {
5445+
return use(thenable);
5446+
}
5447+
5448+
await act(async () => {
5449+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
5450+
pipe(writable);
5451+
});
5452+
expect(getVisibleChildren(container)).toEqual('Hi');
5453+
5454+
ReactDOMClient.hydrateRoot(container, <App />);
5455+
expect(Scheduler).toFlushAndYield([]);
5456+
expect(getVisibleChildren(container)).toEqual('Hi');
5457+
});
52465458
});
52475459
});

0 commit comments

Comments
 (0)