Skip to content

Commit ca01d51

Browse files
committed
Support Promise as React node (Fiber)
Implements Promise as a valid React node type. When the reconciler encounters a promise in a child position, it will transparently unwrap the value before reconciling it. The value of the result will determine the identity of the child during reconciliation, not the promise itself. The Server Components response format can take advantage of this feature by converting lazy child references to promises instead of wrapping them a `React.lazy` element. This also fulfills one of the requirements for async components on the client (note: Server Components can already be written as async functions). However, we will likely warn and/or lint against this for the time being because there are major caveats if you re-render an async component in response to user input. To suspend, it uses the same algorithm as `use`: by throwing an exception to unwind the stack, then replaying the begin phase once the promise resolves. It's a little weird to suspend during reconciliation, however, `lazy` already does this so if there were any obvious bugs related to that we likely would have already found them. Still, the structure is a bit unfortunate. Ideally, we shouldn't need to replay the entire begin phase of the parent fiber in order to reconcile the children again. This would require a somewhat significant refactor, because reconciliation happens deep within the begin phase, and depending on the type of work, not always at the end. We should consider as a future improvement. Unlike `use`, the reconciler will recursively unwrap the value until it reaches a non-Usable type, e.g. Usable<Usable<Usable<T>>> will resolve to T. While eventually we will support all Usable types, Context is not yet supported because it requires a few more steps. I've left this as a to-do. I also haven't yet implemented this in Fizz.
1 parent 1c59fbf commit ca01d51

File tree

5 files changed

+461
-26
lines changed

5 files changed

+461
-26
lines changed

packages/react-reconciler/src/ReactChildFiber.new.js

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
*/
99

1010
import type {ReactElement} from 'shared/ReactElementType';
11-
import type {ReactPortal} from 'shared/ReactTypes';
11+
import type {ReactPortal, Thenable} from 'shared/ReactTypes';
1212
import type {Fiber} from './ReactInternalTypes';
1313
import type {Lanes} from './ReactFiberLane.new';
14+
import type {ThenableState} from './ReactFiberThenable.new';
1415

1516
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
1617
import {
@@ -25,6 +26,8 @@ import {
2526
REACT_FRAGMENT_TYPE,
2627
REACT_PORTAL_TYPE,
2728
REACT_LAZY_TYPE,
29+
REACT_CONTEXT_TYPE,
30+
REACT_SERVER_CONTEXT_TYPE,
2831
} from 'shared/ReactSymbols';
2932
import {ClassComponent, HostText, HostPortal, Fragment} from './ReactWorkTags';
3033
import isArray from 'shared/isArray';
@@ -44,6 +47,11 @@ import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.new';
4447
import {StrictLegacyMode} from './ReactTypeOfMode';
4548
import {getIsHydrating} from './ReactFiberHydrationContext.new';
4649
import {pushTreeFork} from './ReactFiberTreeContext.new';
50+
import {createThenableState, trackUsedThenable} from './ReactFiberThenable.new';
51+
52+
// This tracks the thenables that are unwrapped during reconcilation.
53+
let thenableState: ThenableState | null = null;
54+
let thenableIndexCounter: number = 0;
4755

4856
let didWarnAboutMaps;
4957
let didWarnAboutGenerators;
@@ -98,6 +106,48 @@ if (__DEV__) {
98106
};
99107
}
100108

109+
function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
110+
// Promises are a valid React node type. When the reconciler encounters a
111+
// promise in a child position, it unwraps it using the `use` algorithm: by
112+
// throwing an exception to unwind the stack, then replaying the begin phase
113+
// once the promise resolves.
114+
//
115+
// The structure is a bit unfortunate. Ideally, we shouldn't need to replay
116+
// the entire begin phase of the parent fiber in order to reconcile the
117+
// children again. This would require a somewhat significant refactor, because
118+
// reconcilation happens deep within the begin phase, and depending on the
119+
// type of work, not always at the end. We should consider as an
120+
// future improvement.
121+
//
122+
// Keep unwrapping the value until we reach a non-Usable type.
123+
//
124+
// e.g. Usable<Usable<Usable<T>>> should resolve to T
125+
while (maybeUsable !== null && maybeUsable !== undefined) {
126+
if (typeof maybeUsable.then === 'function') {
127+
// This is a thenable
128+
const thenable: Thenable<any> = (maybeUsable: any);
129+
const index = thenableIndexCounter;
130+
thenableIndexCounter += 1;
131+
132+
if (thenableState === null) {
133+
thenableState = createThenableState();
134+
}
135+
maybeUsable = trackUsedThenable(thenableState, thenable, index);
136+
continue;
137+
} else if (
138+
maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
139+
maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
140+
) {
141+
// TODO: Implement Context as child type.
142+
// const context: ReactContext<mixed> = (maybeUsable: any);
143+
// maybeUsable = readContext(context);
144+
// continue;
145+
}
146+
break;
147+
}
148+
return maybeUsable;
149+
}
150+
101151
function coerceRef(
102152
returnFiber: Fiber,
103153
current: Fiber | null,
@@ -502,6 +552,8 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
502552
newChild: any,
503553
lanes: Lanes,
504554
): Fiber | null {
555+
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
556+
505557
if (
506558
(typeof newChild === 'string' && newChild !== '') ||
507559
typeof newChild === 'number'
@@ -576,6 +628,7 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
576628
lanes: Lanes,
577629
): Fiber | null {
578630
// Update the fiber if the keys match, otherwise return null.
631+
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
579632

580633
const key = oldFiber !== null ? oldFiber.key : null;
581634

@@ -642,6 +695,8 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
642695
newChild: any,
643696
lanes: Lanes,
644697
): Fiber | null {
698+
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
699+
645700
if (
646701
(typeof newChild === 'string' && newChild !== '') ||
647702
typeof newChild === 'number'
@@ -1256,12 +1311,14 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
12561311
// This API will tag the children with the side-effect of the reconciliation
12571312
// itself. They will be added to the side-effect list as we pass through the
12581313
// children and the parent.
1259-
function reconcileChildFibers(
1314+
function reconcileChildFibersImpl(
12601315
returnFiber: Fiber,
12611316
currentFirstChild: Fiber | null,
12621317
newChild: any,
12631318
lanes: Lanes,
12641319
): Fiber | null {
1320+
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
1321+
12651322
// This function is not recursive.
12661323
// If the top level item is an array, we treat it as a set of children,
12671324
// not as a fragment. Nested arrays on the other hand will be treated as
@@ -1357,13 +1414,63 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
13571414
return deleteRemainingChildren(returnFiber, currentFirstChild);
13581415
}
13591416

1360-
return reconcileChildFibers;
1417+
return reconcileChildFibersImpl;
13611418
}
13621419

1363-
export const reconcileChildFibers: ChildReconciler = createChildReconciler(
1420+
export const reconcileChildFibersImpl: ChildReconciler = createChildReconciler(
13641421
true,
13651422
);
1366-
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
1423+
export const mountChildFibersImpl: ChildReconciler = createChildReconciler(
1424+
false,
1425+
);
1426+
1427+
export function reconcileChildFibers(
1428+
returnFiber: Fiber,
1429+
currentFirstChild: Fiber | null,
1430+
newChild: any,
1431+
lanes: Lanes,
1432+
): Fiber | null {
1433+
// This indirection only exists so we can reset `thenableState` at the end.
1434+
// It should get inlined by Closure.
1435+
thenableIndexCounter = 0;
1436+
const firstChildFiber = reconcileChildFibersImpl(
1437+
returnFiber,
1438+
currentFirstChild,
1439+
newChild,
1440+
lanes,
1441+
);
1442+
thenableState = null;
1443+
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
1444+
// set at the beginning.
1445+
return firstChildFiber;
1446+
}
1447+
1448+
export function mountChildFibers(
1449+
returnFiber: Fiber,
1450+
currentFirstChild: Fiber | null,
1451+
newChild: any,
1452+
lanes: Lanes,
1453+
): Fiber | null {
1454+
// This indirection only exists so we can reset `thenableState` at the end.
1455+
// It should get inlined by Closure.
1456+
thenableIndexCounter = 0;
1457+
const firstChildFiber = mountChildFibersImpl(
1458+
returnFiber,
1459+
currentFirstChild,
1460+
newChild,
1461+
lanes,
1462+
);
1463+
thenableState = null;
1464+
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
1465+
// set at the beginning.
1466+
return firstChildFiber;
1467+
}
1468+
1469+
export function resetChildReconcilerOnUnwind(): void {
1470+
// On unwind, clear any pending thenables that were used.
1471+
thenableState = null;
1472+
thenableIndexCounter = 0;
1473+
}
13671474

13681475
export function cloneChildFibers(
13691476
current: Fiber | null,

packages/react-reconciler/src/ReactChildFiber.old.js

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
*/
99

1010
import type {ReactElement} from 'shared/ReactElementType';
11-
import type {ReactPortal} from 'shared/ReactTypes';
11+
import type {ReactPortal, Thenable} from 'shared/ReactTypes';
1212
import type {Fiber} from './ReactInternalTypes';
1313
import type {Lanes} from './ReactFiberLane.old';
14+
import type {ThenableState} from './ReactFiberThenable.old';
1415

1516
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
1617
import {
@@ -25,6 +26,8 @@ import {
2526
REACT_FRAGMENT_TYPE,
2627
REACT_PORTAL_TYPE,
2728
REACT_LAZY_TYPE,
29+
REACT_CONTEXT_TYPE,
30+
REACT_SERVER_CONTEXT_TYPE,
2831
} from 'shared/ReactSymbols';
2932
import {ClassComponent, HostText, HostPortal, Fragment} from './ReactWorkTags';
3033
import isArray from 'shared/isArray';
@@ -44,6 +47,11 @@ import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.old';
4447
import {StrictLegacyMode} from './ReactTypeOfMode';
4548
import {getIsHydrating} from './ReactFiberHydrationContext.old';
4649
import {pushTreeFork} from './ReactFiberTreeContext.old';
50+
import {createThenableState, trackUsedThenable} from './ReactFiberThenable.old';
51+
52+
// This tracks the thenables that are unwrapped during reconcilation.
53+
let thenableState: ThenableState | null = null;
54+
let thenableIndexCounter: number = 0;
4755

4856
let didWarnAboutMaps;
4957
let didWarnAboutGenerators;
@@ -98,6 +106,48 @@ if (__DEV__) {
98106
};
99107
}
100108

109+
function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
110+
// Promises are a valid React node type. When the reconciler encounters a
111+
// promise in a child position, it unwraps it using the `use` algorithm: by
112+
// throwing an exception to unwind the stack, then replaying the begin phase
113+
// once the promise resolves.
114+
//
115+
// The structure is a bit unfortunate. Ideally, we shouldn't need to replay
116+
// the entire begin phase of the parent fiber in order to reconcile the
117+
// children again. This would require a somewhat significant refactor, because
118+
// reconcilation happens deep within the begin phase, and depending on the
119+
// type of work, not always at the end. We should consider as an
120+
// future improvement.
121+
//
122+
// Keep unwrapping the value until we reach a non-Usable type.
123+
//
124+
// e.g. Usable<Usable<Usable<T>>> should resolve to T
125+
while (maybeUsable !== null && maybeUsable !== undefined) {
126+
if (typeof maybeUsable.then === 'function') {
127+
// This is a thenable
128+
const thenable: Thenable<any> = (maybeUsable: any);
129+
const index = thenableIndexCounter;
130+
thenableIndexCounter += 1;
131+
132+
if (thenableState === null) {
133+
thenableState = createThenableState();
134+
}
135+
maybeUsable = trackUsedThenable(thenableState, thenable, index);
136+
continue;
137+
} else if (
138+
maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
139+
maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
140+
) {
141+
// TODO: Implement Context as child type.
142+
// const context: ReactContext<mixed> = (maybeUsable: any);
143+
// maybeUsable = readContext(context);
144+
// continue;
145+
}
146+
break;
147+
}
148+
return maybeUsable;
149+
}
150+
101151
function coerceRef(
102152
returnFiber: Fiber,
103153
current: Fiber | null,
@@ -502,6 +552,8 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
502552
newChild: any,
503553
lanes: Lanes,
504554
): Fiber | null {
555+
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
556+
505557
if (
506558
(typeof newChild === 'string' && newChild !== '') ||
507559
typeof newChild === 'number'
@@ -576,6 +628,7 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
576628
lanes: Lanes,
577629
): Fiber | null {
578630
// Update the fiber if the keys match, otherwise return null.
631+
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
579632

580633
const key = oldFiber !== null ? oldFiber.key : null;
581634

@@ -642,6 +695,8 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
642695
newChild: any,
643696
lanes: Lanes,
644697
): Fiber | null {
698+
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
699+
645700
if (
646701
(typeof newChild === 'string' && newChild !== '') ||
647702
typeof newChild === 'number'
@@ -1256,12 +1311,14 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
12561311
// This API will tag the children with the side-effect of the reconciliation
12571312
// itself. They will be added to the side-effect list as we pass through the
12581313
// children and the parent.
1259-
function reconcileChildFibers(
1314+
function reconcileChildFibersImpl(
12601315
returnFiber: Fiber,
12611316
currentFirstChild: Fiber | null,
12621317
newChild: any,
12631318
lanes: Lanes,
12641319
): Fiber | null {
1320+
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
1321+
12651322
// This function is not recursive.
12661323
// If the top level item is an array, we treat it as a set of children,
12671324
// not as a fragment. Nested arrays on the other hand will be treated as
@@ -1357,13 +1414,63 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
13571414
return deleteRemainingChildren(returnFiber, currentFirstChild);
13581415
}
13591416

1360-
return reconcileChildFibers;
1417+
return reconcileChildFibersImpl;
13611418
}
13621419

1363-
export const reconcileChildFibers: ChildReconciler = createChildReconciler(
1420+
export const reconcileChildFibersImpl: ChildReconciler = createChildReconciler(
13641421
true,
13651422
);
1366-
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
1423+
export const mountChildFibersImpl: ChildReconciler = createChildReconciler(
1424+
false,
1425+
);
1426+
1427+
export function reconcileChildFibers(
1428+
returnFiber: Fiber,
1429+
currentFirstChild: Fiber | null,
1430+
newChild: any,
1431+
lanes: Lanes,
1432+
): Fiber | null {
1433+
// This indirection only exists so we can reset `thenableState` at the end.
1434+
// It should get inlined by Closure.
1435+
thenableIndexCounter = 0;
1436+
const firstChildFiber = reconcileChildFibersImpl(
1437+
returnFiber,
1438+
currentFirstChild,
1439+
newChild,
1440+
lanes,
1441+
);
1442+
thenableState = null;
1443+
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
1444+
// set at the beginning.
1445+
return firstChildFiber;
1446+
}
1447+
1448+
export function mountChildFibers(
1449+
returnFiber: Fiber,
1450+
currentFirstChild: Fiber | null,
1451+
newChild: any,
1452+
lanes: Lanes,
1453+
): Fiber | null {
1454+
// This indirection only exists so we can reset `thenableState` at the end.
1455+
// It should get inlined by Closure.
1456+
thenableIndexCounter = 0;
1457+
const firstChildFiber = mountChildFibersImpl(
1458+
returnFiber,
1459+
currentFirstChild,
1460+
newChild,
1461+
lanes,
1462+
);
1463+
thenableState = null;
1464+
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
1465+
// set at the beginning.
1466+
return firstChildFiber;
1467+
}
1468+
1469+
export function resetChildReconcilerOnUnwind(): void {
1470+
// On unwind, clear any pending thenables that were used.
1471+
thenableState = null;
1472+
thenableIndexCounter = 0;
1473+
}
13671474

13681475
export function cloneChildFibers(
13691476
current: Fiber | null,

0 commit comments

Comments
 (0)