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.
This commit is contained in:
Sebastian Markbåge
2022-10-14 15:09:33 -04:00
committed by GitHub
parent 44e2ca393e
commit 3b814327e2
2 changed files with 43 additions and 15 deletions
@@ -350,25 +350,19 @@ describe('ReactFlightDOM', () => {
}
function makeDelayedText() {
let error, _resolve, _reject;
let _resolve, _reject;
let promise = new Promise((resolve, reject) => {
_resolve = () => {
promise = null;
resolve();
};
_reject = e => {
error = e;
promise = null;
reject(e);
};
});
function DelayedText({children}, data) {
if (promise) {
throw promise;
}
if (error) {
throw error;
}
async function DelayedText({children}) {
await promise;
return <Text>{children}</Text>;
}
return [DelayedText, _resolve, _reject];
@@ -469,7 +463,9 @@ describe('ReactFlightDOM', () => {
resolveName();
});
// Advance time enough to trigger a nested fallback.
jest.advanceTimersByTime(500);
await act(async () => {
jest.advanceTimersByTime(500);
});
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
'<p>(loading sidebar)</p>' +
@@ -482,7 +478,8 @@ describe('ReactFlightDOM', () => {
const theError = new Error('Game over');
// Let's *fail* loading games.
await act(async () => {
rejectGames(theError);
await rejectGames(theError);
await 'the inner async function';
});
const expectedGamesValue = __DEV__
? '<p>Game over + a dev digest</p>'
@@ -499,7 +496,8 @@ describe('ReactFlightDOM', () => {
// We can now show the sidebar.
await act(async () => {
resolvePhotos();
await resolvePhotos();
await 'the inner async function';
});
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
@@ -510,7 +508,8 @@ describe('ReactFlightDOM', () => {
// Show everything.
await act(async () => {
resolvePosts();
await resolvePosts();
await 'the inner async function';
});
expect(container.innerHTML).toBe(
'<div>:name::avatar:</div>' +
+31 -2
View File
@@ -21,7 +21,9 @@ import type {
ReactProviderType,
ServerContextJSONValue,
Wakeable,
Thenable,
} from 'shared/ReactTypes';
import type {LazyComponent} from 'react/src/ReactLazy';
import {
scheduleWork,
@@ -87,6 +89,7 @@ type ReactJSONValue =
export type ReactModel =
| React$Element<any>
| LazyComponent<any, any>
| string
| boolean
| number
@@ -192,6 +195,25 @@ function createRootContext(
const POP = {};
function readThenable<T>(thenable: Thenable<T>): T {
if (thenable.status === 'fulfilled') {
return thenable.value;
} else if (thenable.status === 'rejected') {
throw thenable.reason;
}
throw thenable;
}
function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
trackSuspendedWakeable(wakeable);
const lazyType: LazyComponent<any, Thenable<any>> = {
$$typeof: REACT_LAZY_TYPE,
_payload: (wakeable: any),
_init: readThenable,
};
return lazyType;
}
function attemptResolveElement(
type: any,
key: null | React$Key,
@@ -214,7 +236,15 @@ function attemptResolveElement(
}
// This is a server-side component.
prepareToUseHooksForComponent(prevThenableState);
return type(props);
const result = type(props);
if (
typeof result === 'object' &&
result !== null &&
typeof result.then === 'function'
) {
return createLazyWrapperAroundWakeable(result);
}
return result;
} else if (typeof type === 'string') {
// This is a host element. E.g. HTML.
return [REACT_ELEMENT_TYPE, type, key, props];
@@ -636,7 +666,6 @@ export function resolveModelToJSON(
return serializeByRefID(newTask.id);
} else {
logRecoverableError(request, x);
// Something errored. We'll still send everything we have up until this point.
// We'll replace this element with a lazy reference that throws on the client
// once it gets rendered.