diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index f6a6a262a58ee..3d00423c9a081 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -25,6 +25,7 @@ let Suspense;
let SuspenseList;
let useSyncExternalStore;
let useSyncExternalStoreWithSelector;
+let use;
let PropTypes;
let textCache;
let window;
@@ -47,6 +48,7 @@ describe('ReactDOMFizzServer', () => {
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
Suspense = React.Suspense;
+ use = React.use;
if (gate(flags => flags.enableSuspenseList)) {
SuspenseList = React.SuspenseList;
}
@@ -5166,6 +5168,216 @@ 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 (
+
+
+
+ );
+ }
+
+ await act(async () => {
+ const {pipe} = renderToPipeableStream();
+ 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, );
+ 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 ;
+ }
+
+ function App() {
+ return (
+
+
+
+ );
+ }
+
+ await act(async () => {
+ const {pipe} = renderToPipeableStream();
+ 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, );
+ 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 (
+
+
+
+
+
+ );
+ }
+
+ const reportedServerErrors = [];
+ await act(async () => {
+ const {pipe} = renderToPipeableStream(, {
+ 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, , {
+ 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} = renderToPipeableStream();
+ pipe(writable);
+ });
+ expect(getVisibleChildren(container)).toEqual('Hi');
+
+ ReactDOMClient.hydrateRoot(container, );
+ expect(Scheduler).toFlushAndYield([]);
+ expect(getVisibleChildren(container)).toEqual('Hi');
+ });
});
describe('useEvent', () => {
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 5c6547bd33721..05b7dd63d6dec 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
@@ -462,7 +462,6 @@ describe('ReactFlightDOMBrowser', () => {
});
// @gate enableUseHook
- // @gate FIXME // Depends on `use` (which was temporarily reverted in Fizz)
it('should allow an alternative module mapping to be used for SSR', async () => {
function ClientComponent() {
return Client Component;
diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js
index 2f68ec2561e69..39f0af172fabe 100644
--- a/packages/react-server/src/ReactFizzHooks.js
+++ b/packages/react-server/src/ReactFizzHooks.js
@@ -15,18 +15,27 @@ import type {
MutableSourceSubscribeFn,
ReactContext,
StartTransitionOptions,
+ Thenable,
+ Usable,
} from 'shared/ReactTypes';
import type {ResponseState} from './ReactServerFormatConfig';
import type {Task} from './ReactFizzServer';
+import type {ThenableState} from './ReactFizzWakeable';
import {readContext as readContextImpl} from './ReactFizzNewContext';
import {getTreeId} from './ReactFizzTreeContext';
+import {
+ getPreviouslyUsedThenableAtIndex,
+ createThenableState,
+ trackUsedThenable,
+} from './ReactFizzWakeable';
import {makeId} from './ReactServerFormatConfig';
import {
enableCache,
+ enableUseHook,
enableUseEventHook,
enableUseMemoCacheHook,
} from 'shared/ReactFeatureFlags';
@@ -62,6 +71,9 @@ let isReRender: boolean = false;
let didScheduleRenderPhaseUpdate: boolean = false;
// Counts the number of useId hooks in this component
let localIdCounter: number = 0;
+// Counts the number of use(thenable) calls in this component
+let thenableIndexCounter: number = 0;
+let thenableState: ThenableState | null = null;
// Lazily created map of render-phase updates
let renderPhaseUpdates: Map, Update> | null = null;
// Counter to prevent infinite loops.
@@ -176,7 +188,11 @@ function createWorkInProgressHook(): Hook {
return workInProgressHook;
}
-export function prepareToUseHooks(task: Task, componentIdentity: Object): void {
+export function prepareToUseHooks(
+ task: Task,
+ componentIdentity: Object,
+ prevThenableState: ThenableState | null,
+): void {
currentlyRenderingComponent = componentIdentity;
currentlyRenderingTask = task;
if (__DEV__) {
@@ -191,6 +207,8 @@ export function prepareToUseHooks(task: Task, componentIdentity: Object): void {
// workInProgressHook = null;
localIdCounter = 0;
+ thenableIndexCounter = 0;
+ thenableState = prevThenableState;
}
export function finishHooks(
@@ -209,6 +227,7 @@ export function finishHooks(
// restarting until no more updates are scheduled.
didScheduleRenderPhaseUpdate = false;
localIdCounter = 0;
+ thenableIndexCounter = 0;
numberOfReRenders += 1;
// Start over from the beginning of the list
@@ -220,6 +239,12 @@ export function finishHooks(
return children;
}
+export function getThenableStateAfterSuspending(): null | ThenableState {
+ const state = thenableState;
+ thenableState = null;
+ return state;
+}
+
export function checkDidRenderIdHook(): boolean {
// This should be called immediately after every finishHooks call.
// Conceptually, it's part of the return value of finishHooks; it's only a
@@ -553,6 +578,81 @@ function useId(): string {
return makeId(responseState, treeId, localId);
}
+function use(usable: Usable): T {
+ if (usable !== null && typeof usable === 'object') {
+ // $FlowFixMe[method-unbinding]
+ if (typeof usable.then === 'function') {
+ // This is a thenable.
+ const thenable: Thenable = (usable: any);
+
+ // Track the position of the thenable within this fiber.
+ const index = thenableIndexCounter;
+ thenableIndexCounter += 1;
+
+ switch (thenable.status) {
+ case 'fulfilled': {
+ const fulfilledValue: T = thenable.value;
+ return fulfilledValue;
+ }
+ case 'rejected': {
+ const rejectedError = thenable.reason;
+ throw rejectedError;
+ }
+ default: {
+ const prevThenableAtIndex: Thenable | null = getPreviouslyUsedThenableAtIndex(
+ thenableState,
+ index,
+ );
+ if (prevThenableAtIndex !== null) {
+ if (thenable !== prevThenableAtIndex) {
+ // Avoid an unhandled rejection errors for the Promises that we'll
+ // intentionally ignore.
+ thenable.then(noop, noop);
+ }
+ switch (prevThenableAtIndex.status) {
+ case 'fulfilled': {
+ const fulfilledValue: T = prevThenableAtIndex.value;
+ return fulfilledValue;
+ }
+ case 'rejected': {
+ const rejectedError: mixed = prevThenableAtIndex.reason;
+ throw rejectedError;
+ }
+ default: {
+ // The thenable still hasn't resolved. Suspend with the same
+ // thenable as last time to avoid redundant listeners.
+ throw prevThenableAtIndex;
+ }
+ }
+ } else {
+ // This is the first time something has been used at this index.
+ // Stash the thenable at the current index so we can reuse it during
+ // the next attempt.
+ if (thenableState === null) {
+ thenableState = createThenableState();
+ }
+ trackUsedThenable(thenableState, thenable, index);
+
+ // Suspend.
+ // TODO: Throwing here is an implementation detail that allows us to
+ // unwind the call stack. But we shouldn't allow it to leak into
+ // userspace. Throw an opaque placeholder value instead of the
+ // actual thenable. If it doesn't get captured by the work loop, log
+ // a warning, because that means something in userspace must have
+ // caught it.
+ throw thenable;
+ }
+ }
+ }
+ } else {
+ // TODO: Add support for Context
+ }
+ }
+
+ // eslint-disable-next-line react-internal/safe-string-coercion
+ throw new Error('An unsupported type was passed to use(): ' + String(usable));
+}
+
function unsupportedRefresh() {
throw new Error('Cache cannot be refreshed during server rendering.');
}
@@ -604,6 +704,9 @@ if (enableUseEventHook) {
if (enableUseMemoCacheHook) {
HooksDispatcher.useMemoCache = useMemoCache;
}
+if (enableUseHook) {
+ HooksDispatcher.use = use;
+}
export let currentResponseState: null | ResponseState = (null: any);
export function setCurrentResponseState(
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 3053d3dc3dfe5..82191e9914bc2 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -17,6 +17,7 @@ import type {
ReactContext,
ReactProviderType,
OffscreenMode,
+ Wakeable,
} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {
@@ -29,6 +30,7 @@ import type {
import type {ContextSnapshot} from './ReactFizzNewContext';
import type {ComponentStackNode} from './ReactFizzComponentStack';
import type {TreeContext} from './ReactFizzTreeContext';
+import type {ThenableState} from './ReactFizzWakeable';
import {
scheduleWork,
@@ -98,6 +100,7 @@ import {
HooksDispatcher,
currentResponseState,
setCurrentResponseState,
+ getThenableStateAfterSuspending,
} from './ReactFizzHooks';
import {DefaultCacheDispatcher} from './ReactFizzCache';
import {getStackByComponentStackNode} from './ReactFizzComponentStack';
@@ -136,6 +139,7 @@ import {
import assign from 'shared/assign';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import isArray from 'shared/isArray';
+import {trackSuspendedWakeable} from './ReactFizzWakeable';
const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
const ReactCurrentCache = ReactSharedInternals.ReactCurrentCache;
@@ -170,6 +174,7 @@ export type Task = {
context: ContextSnapshot, // the current new context that this task is executing in
treeContext: TreeContext, // the current tree context that this task is executing in
componentStack: null | ComponentStackNode, // DEV-only component stack
+ thenableState: null | ThenableState,
};
const PENDING = 0;
@@ -315,6 +320,7 @@ export function createRequest(
rootSegment.parentFlushed = true;
const rootTask = createTask(
request,
+ null,
children,
null,
rootSegment,
@@ -355,6 +361,7 @@ function createSuspenseBoundary(
function createTask(
request: Request,
+ thenableState: ThenableState | null,
node: ReactNodeList,
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment,
@@ -378,6 +385,7 @@ function createTask(
legacyContext,
context,
treeContext,
+ thenableState,
}: any);
if (__DEV__) {
task.componentStack = null;
@@ -628,6 +636,7 @@ function renderSuspenseBoundary(
// on it yet in case we finish the main content, so we queue for later.
const suspendedFallbackTask = createTask(
request,
+ null,
fallback,
parentBoundary,
boundarySegment,
@@ -717,12 +726,13 @@ function shouldConstruct(Component) {
function renderWithHooks(
request: Request,
task: Task,
+ prevThenableState: ThenableState | null,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
): any {
const componentIdentity = {};
- prepareToUseHooks(task, componentIdentity);
+ prepareToUseHooks(task, componentIdentity, prevThenableState);
const result = Component(props, secondArg);
return finishHooks(Component, props, result, secondArg);
}
@@ -760,13 +770,13 @@ function finishClassComponent(
childContextTypes,
);
task.legacyContext = mergedContext;
- renderNodeDestructive(request, task, nextChildren);
+ renderNodeDestructive(request, task, null, nextChildren);
task.legacyContext = previousContext;
return;
}
}
- renderNodeDestructive(request, task, nextChildren);
+ renderNodeDestructive(request, task, null, nextChildren);
}
function renderClassComponent(
@@ -800,6 +810,7 @@ let hasWarnedAboutUsingContextAsConsumer = false;
function renderIndeterminateComponent(
request: Request,
task: Task,
+ prevThenableState: ThenableState | null,
Component: any,
props: any,
): void {
@@ -828,7 +839,14 @@ function renderIndeterminateComponent(
}
}
- const value = renderWithHooks(request, task, Component, props, legacyContext);
+ const value = renderWithHooks(
+ request,
+ task,
+ prevThenableState,
+ Component,
+ props,
+ legacyContext,
+ );
const hasId = checkDidRenderIdHook();
if (__DEV__) {
@@ -909,12 +927,12 @@ function renderIndeterminateComponent(
const index = 0;
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
try {
- renderNodeDestructive(request, task, value);
+ renderNodeDestructive(request, task, null, value);
} finally {
task.treeContext = prevTreeContext;
}
} else {
- renderNodeDestructive(request, task, value);
+ renderNodeDestructive(request, task, null, value);
}
}
popComponentStackInDEV(task);
@@ -994,12 +1012,20 @@ function resolveDefaultProps(Component: any, baseProps: Object): Object {
function renderForwardRef(
request: Request,
task: Task,
+ prevThenableState,
type: any,
props: Object,
ref: any,
): void {
pushFunctionComponentStackInDEV(task, type.render);
- const children = renderWithHooks(request, task, type.render, props, ref);
+ const children = renderWithHooks(
+ request,
+ task,
+ prevThenableState,
+ type.render,
+ props,
+ ref,
+ );
const hasId = checkDidRenderIdHook();
if (hasId) {
// This component materialized an id. We treat this as its own level, with
@@ -1009,12 +1035,12 @@ function renderForwardRef(
const index = 0;
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
try {
- renderNodeDestructive(request, task, children);
+ renderNodeDestructive(request, task, null, children);
} finally {
task.treeContext = prevTreeContext;
}
} else {
- renderNodeDestructive(request, task, children);
+ renderNodeDestructive(request, task, null, children);
}
popComponentStackInDEV(task);
}
@@ -1022,13 +1048,21 @@ function renderForwardRef(
function renderMemo(
request: Request,
task: Task,
+ prevThenableState: ThenableState | null,
type: any,
props: Object,
ref: any,
): void {
const innerType = type.type;
const resolvedProps = resolveDefaultProps(innerType, props);
- renderElement(request, task, innerType, resolvedProps, ref);
+ renderElement(
+ request,
+ task,
+ prevThenableState,
+ innerType,
+ resolvedProps,
+ ref,
+ );
}
function renderContextConsumer(
@@ -1078,7 +1112,7 @@ function renderContextConsumer(
const newValue = readContext(context);
const newChildren = render(newValue);
- renderNodeDestructive(request, task, newChildren);
+ renderNodeDestructive(request, task, null, newChildren);
}
function renderContextProvider(
@@ -1095,7 +1129,7 @@ function renderContextProvider(
prevSnapshot = task.context;
}
task.context = pushProvider(context, value);
- renderNodeDestructive(request, task, children);
+ renderNodeDestructive(request, task, null, children);
task.context = popProvider(context);
if (__DEV__) {
if (prevSnapshot !== task.context) {
@@ -1109,6 +1143,7 @@ function renderContextProvider(
function renderLazyComponent(
request: Request,
task: Task,
+ prevThenableState: ThenableState | null,
lazyComponent: LazyComponentType,
props: Object,
ref: any,
@@ -1118,7 +1153,14 @@ function renderLazyComponent(
const init = lazyComponent._init;
const Component = init(payload);
const resolvedProps = resolveDefaultProps(Component, props);
- renderElement(request, task, Component, resolvedProps, ref);
+ renderElement(
+ request,
+ task,
+ prevThenableState,
+ Component,
+ resolvedProps,
+ ref,
+ );
popComponentStackInDEV(task);
}
@@ -1130,13 +1172,14 @@ function renderOffscreen(request: Request, task: Task, props: Object): void {
} else {
// A visible Offscreen boundary is treated exactly like a fragment: a
// pure indirection.
- renderNodeDestructive(request, task, props.children);
+ renderNodeDestructive(request, task, null, props.children);
}
}
function renderElement(
request: Request,
task: Task,
+ prevThenableState: ThenableState | null,
type: any,
props: Object,
ref: any,
@@ -1146,7 +1189,13 @@ function renderElement(
renderClassComponent(request, task, type, props);
return;
} else {
- renderIndeterminateComponent(request, task, type, props);
+ renderIndeterminateComponent(
+ request,
+ task,
+ prevThenableState,
+ type,
+ props,
+ );
return;
}
}
@@ -1170,7 +1219,7 @@ function renderElement(
case REACT_STRICT_MODE_TYPE:
case REACT_PROFILER_TYPE:
case REACT_FRAGMENT_TYPE: {
- renderNodeDestructive(request, task, props.children);
+ renderNodeDestructive(request, task, null, props.children);
return;
}
case REACT_OFFSCREEN_TYPE: {
@@ -1180,13 +1229,13 @@ function renderElement(
case REACT_SUSPENSE_LIST_TYPE: {
pushBuiltInComponentStackInDEV(task, 'SuspenseList');
// TODO: SuspenseList should control the boundaries.
- renderNodeDestructive(request, task, props.children);
+ renderNodeDestructive(request, task, null, props.children);
popComponentStackInDEV(task);
return;
}
case REACT_SCOPE_TYPE: {
if (enableScopeAPI) {
- renderNodeDestructive(request, task, props.children);
+ renderNodeDestructive(request, task, null, props.children);
return;
}
throw new Error('ReactDOMServer does not yet support scope components.');
@@ -1208,11 +1257,11 @@ function renderElement(
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
case REACT_FORWARD_REF_TYPE: {
- renderForwardRef(request, task, type, props, ref);
+ renderForwardRef(request, task, prevThenableState, type, props, ref);
return;
}
case REACT_MEMO_TYPE: {
- renderMemo(request, task, type, props, ref);
+ renderMemo(request, task, prevThenableState, type, props, ref);
return;
}
case REACT_PROVIDER_TYPE: {
@@ -1224,7 +1273,7 @@ function renderElement(
return;
}
case REACT_LAZY_TYPE: {
- renderLazyComponent(request, task, type, props);
+ renderLazyComponent(request, task, prevThenableState, type, props);
return;
}
}
@@ -1289,6 +1338,9 @@ function validateIterable(iterable, iteratorFn: Function): void {
function renderNodeDestructive(
request: Request,
task: Task,
+ // The thenable state reused from the previous attempt, if any. This is almost
+ // always null, except when called by retryTask.
+ prevThenableState: ThenableState | null,
node: ReactNodeList,
): void {
if (__DEV__) {
@@ -1296,7 +1348,7 @@ function renderNodeDestructive(
// a component stack at the right place in the tree. We don't do this in renderNode
// becuase it is not called at every layer of the tree and we may lose frames
try {
- return renderNodeDestructiveImpl(request, task, node);
+ return renderNodeDestructiveImpl(request, task, prevThenableState, node);
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// This is a Wakable, noop
@@ -1311,7 +1363,7 @@ function renderNodeDestructive(
throw x;
}
} else {
- return renderNodeDestructiveImpl(request, task, node);
+ return renderNodeDestructiveImpl(request, task, prevThenableState, node);
}
}
@@ -1320,6 +1372,7 @@ function renderNodeDestructive(
function renderNodeDestructiveImpl(
request: Request,
task: Task,
+ prevThenableState: ThenableState | null,
node: ReactNodeList,
): void {
// Stash the node we're working on. We'll pick up from this task in case
@@ -1334,7 +1387,7 @@ function renderNodeDestructiveImpl(
const type = element.type;
const props = element.props;
const ref = element.ref;
- renderElement(request, task, type, props, ref);
+ renderElement(request, task, prevThenableState, type, props, ref);
return;
}
case REACT_PORTAL_TYPE:
@@ -1368,7 +1421,7 @@ function renderNodeDestructiveImpl(
} else {
resolvedNode = init(payload);
}
- renderNodeDestructive(request, task, resolvedNode);
+ renderNodeDestructive(request, task, null, resolvedNode);
return;
}
}
@@ -1470,6 +1523,7 @@ function renderChildrenArray(request, task, children) {
function spawnNewSuspendedTask(
request: Request,
task: Task,
+ thenableState: ThenableState | null,
x: Promise,
): void {
// Something suspended, we'll need to create a new segment and resolve it later.
@@ -1490,6 +1544,7 @@ function spawnNewSuspendedTask(
segment.lastPushedText = false;
const newTask = createTask(
request,
+ thenableState,
task.node,
task.blockedBoundary,
newSegment,
@@ -1498,6 +1553,9 @@ function spawnNewSuspendedTask(
task.context,
task.treeContext,
);
+
+ trackSuspendedWakeable(x);
+
if (__DEV__) {
if (task.componentStack !== null) {
// We pop one task off the stack because the node that suspended will be tried again,
@@ -1525,11 +1583,13 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
previousComponentStack = task.componentStack;
}
try {
- return renderNodeDestructive(request, task, node);
+ return renderNodeDestructive(request, task, null, node);
} catch (x) {
resetHooksState();
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
- spawnNewSuspendedTask(request, task, x);
+ const thenableState = getThenableStateAfterSuspending();
+ spawnNewSuspendedTask(request, task, thenableState, x);
+
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
task.blockedSegment.formatContext = previousFormatContext;
@@ -1795,7 +1855,14 @@ function retryTask(request: Request, task: Task): void {
try {
// We call the destructive form that mutates this task. That way if something
// suspends again, we can reuse the same task instead of spawning a new one.
- renderNodeDestructive(request, task, task.node);
+
+ // Reset the task's thenable state before continuing, so that if a later
+ // component suspends we can reuse the same task object. If the same
+ // component suspends again, the thenable state will be restored.
+ const prevThenableState = task.thenableState;
+ task.thenableState = null;
+
+ renderNodeDestructive(request, task, prevThenableState, task.node);
pushSegmentFinale(
segment.chunks,
request.responseState,
@@ -1812,6 +1879,10 @@ function retryTask(request: Request, task: Task): void {
// Something suspended again, let's pick it back up later.
const ping = task.ping;
x.then(ping, ping);
+
+ const wakeable: Wakeable = x;
+ trackSuspendedWakeable(wakeable);
+ task.thenableState = getThenableStateAfterSuspending();
} else {
task.abortSet.delete(task);
segment.status = ERRORED;