Allow Async Functions to be used in Server Components (#25479)
This is a temporary step until we allow Promises everywhere. Currently this serializes to a Lazy which can then be consumed in this same slot by the client.
Sebastian Markbåge committed
Oct 14, 2022 at 15:09 UTC
3b814327e2390e6f3dfd4c855d849662ddd52b2a
2 files changed
+43
-15
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+12
-13
@@ -350,25 +350,19 @@ describe('ReactFlightDOM', () => {
350
}
351
352
function makeDelayedText() {
353
- let error, _resolve, _reject;
353
+ let _resolve, _reject;
354
let promise = new Promise((resolve, reject) => {
355
_resolve = () => {
356
promise = null;
357
resolve();
358
};
359
_reject = e => {
360
- error = e;
360
promise = null;
361
reject(e);
362
};
363
});
365
- function DelayedText({children}, data) {
366
- if (promise) {
367
- throw promise;
368
- }
369
- if (error) {
370
- throw error;
371
- }
364
+ async function DelayedText({children}) {
365
+ await promise;
366
return <Text>{children}</Text>;
367
}
368
return [DelayedText, _resolve, _reject];
@@ -469,7 +463,9 @@ describe('ReactFlightDOM', () => {
463
resolveName();
464
});
465
// Advance time enough to trigger a nested fallback.
472
- jest.advanceTimersByTime(500);
466
+ await act(async () => {
467
+ jest.advanceTimersByTime(500);
468
+ });
469
expect(container.innerHTML).toBe(
470
'<div>:name::avatar:</div>' +
471
'<p>(loading sidebar)</p>' +
@@ -482,7 +478,8 @@ describe('ReactFlightDOM', () => {
478
const theError = new Error('Game over');
479
// Let's *fail* loading games.
480
await act(async () => {
485
- rejectGames(theError);
481
+ await rejectGames(theError);
482
+ await 'the inner async function';
483
});
484
const expectedGamesValue = __DEV__
485
? '<p>Game over + a dev digest</p>'
@@ -499,7 +496,8 @@ describe('ReactFlightDOM', () => {
496
497
// We can now show the sidebar.
498
await act(async () => {
502
- resolvePhotos();
499
+ await resolvePhotos();
500
+ await 'the inner async function';
501
});
502
expect(container.innerHTML).toBe(
503
'<div>:name::avatar:</div>' +
@@ -510,7 +508,8 @@ describe('ReactFlightDOM', () => {
508
509
// Show everything.
510
await act(async () => {
513
- resolvePosts();
511
+ await resolvePosts();
512
+ await 'the inner async function';
513
});
514
expect(container.innerHTML).toBe(
515
'<div>:name::avatar:</div>' +
packages/react-server/src/ReactFlightServer.js
+31
-2
@@ -21,7 +21,9 @@ import type {
21
ReactProviderType,
22
ServerContextJSONValue,
23
Wakeable,
24
+ Thenable,
25
} from 'shared/ReactTypes';
26
+import type {LazyComponent} from 'react/src/ReactLazy';
27
28
import {
29
scheduleWork,
@@ -87,6 +89,7 @@ type ReactJSONValue =
89
90
export type ReactModel =
91
| React$Element<any>
92
+ | LazyComponent<any, any>
93
| string
94
| boolean
95
| number
@@ -192,6 +195,25 @@ function createRootContext(
195
196
const POP = {};
197
198
+function readThenable<T>(thenable: Thenable<T>): T {
199
+ if (thenable.status === 'fulfilled') {
200
+ return thenable.value;
201
+ } else if (thenable.status === 'rejected') {
202
+ throw thenable.reason;
203
+ }
204
+ throw thenable;
205
+}
206
+
207
+function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
208
+ trackSuspendedWakeable(wakeable);
209
+ const lazyType: LazyComponent<any, Thenable<any>> = {
210
+ $$typeof: REACT_LAZY_TYPE,
211
+ _payload: (wakeable: any),
212
+ _init: readThenable,
213
+ };
214
+ return lazyType;
215
+}
216
+
217
function attemptResolveElement(
218
type: any,
219
key: null | React$Key,
@@ -214,7 +236,15 @@ function attemptResolveElement(
236
}
237
// This is a server-side component.
238
prepareToUseHooksForComponent(prevThenableState);
217
- return type(props);
239
+ const result = type(props);
240
+ if (
241
+ typeof result === 'object' &&
242
+ result !== null &&
243
+ typeof result.then === 'function'
244
+ ) {
245
+ return createLazyWrapperAroundWakeable(result);
246
+ }
247
+ return result;
248
} else if (typeof type === 'string') {
249
// This is a host element. E.g. HTML.
250
return [REACT_ELEMENT_TYPE, type, key, props];
@@ -636,7 +666,6 @@ export function resolveModelToJSON(
666
667
return serializeByRefID(newTask.id);
668
} else {
639
- logRecoverableError(request, x);
669
// Something errored. We'll still send everything we have up until this point.
670
// We'll replace this element with a lazy reference that throws on the client
671
// once it gets rendered.