Skip to content

Commit

Permalink
Support Promise as React node (Fiber)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Nov 4, 2022
1 parent 1c59fbf commit ca01d51
Show file tree
Hide file tree
Showing 5 changed files with 461 additions and 26 deletions.
117 changes: 112 additions & 5 deletions packages/react-reconciler/src/ReactChildFiber.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
*/

import type {ReactElement} from 'shared/ReactElementType';
import type {ReactPortal} from 'shared/ReactTypes';
import type {ReactPortal, Thenable} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.new';
import type {ThenableState} from './ReactFiberThenable.new';

import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import {
Expand All @@ -25,6 +26,8 @@ import {
REACT_FRAGMENT_TYPE,
REACT_PORTAL_TYPE,
REACT_LAZY_TYPE,
REACT_CONTEXT_TYPE,
REACT_SERVER_CONTEXT_TYPE,
} from 'shared/ReactSymbols';
import {ClassComponent, HostText, HostPortal, Fragment} from './ReactWorkTags';
import isArray from 'shared/isArray';
Expand All @@ -44,6 +47,11 @@ import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.new';
import {StrictLegacyMode} from './ReactTypeOfMode';
import {getIsHydrating} from './ReactFiberHydrationContext.new';
import {pushTreeFork} from './ReactFiberTreeContext.new';
import {createThenableState, trackUsedThenable} from './ReactFiberThenable.new';

// This tracks the thenables that are unwrapped during reconcilation.
let thenableState: ThenableState | null = null;
let thenableIndexCounter: number = 0;

let didWarnAboutMaps;
let didWarnAboutGenerators;
Expand Down Expand Up @@ -98,6 +106,48 @@ if (__DEV__) {
};
}

function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
// Promises are a valid React node type. When the reconciler encounters a
// promise in a child position, it unwraps it using the `use` algorithm: by
// throwing an exception to unwind the stack, then replaying the begin phase
// once the promise resolves.
//
// 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
// reconcilation happens deep within the begin phase, and depending on the
// type of work, not always at the end. We should consider as an
// future improvement.
//
// Keep unwrapping the value until we reach a non-Usable type.
//
// e.g. Usable<Usable<Usable<T>>> should resolve to T
while (maybeUsable !== null && maybeUsable !== undefined) {
if (typeof maybeUsable.then === 'function') {
// This is a thenable
const thenable: Thenable<any> = (maybeUsable: any);
const index = thenableIndexCounter;
thenableIndexCounter += 1;

if (thenableState === null) {
thenableState = createThenableState();
}
maybeUsable = trackUsedThenable(thenableState, thenable, index);
continue;
} else if (
maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
// const context: ReactContext<mixed> = (maybeUsable: any);
// maybeUsable = readContext(context);
// continue;
}
break;
}
return maybeUsable;
}

function coerceRef(
returnFiber: Fiber,
current: Fiber | null,
Expand Down Expand Up @@ -502,6 +552,8 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);

if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
Expand Down Expand Up @@ -576,6 +628,7 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
lanes: Lanes,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);

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

Expand Down Expand Up @@ -642,6 +695,8 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);

if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
Expand Down Expand Up @@ -1256,12 +1311,14 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
// This API will tag the children with the side-effect of the reconciliation
// itself. They will be added to the side-effect list as we pass through the
// children and the parent.
function reconcileChildFibers(
function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);

// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
Expand Down Expand Up @@ -1357,13 +1414,63 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

return reconcileChildFibers;
return reconcileChildFibersImpl;
}

export const reconcileChildFibers: ChildReconciler = createChildReconciler(
export const reconcileChildFibersImpl: ChildReconciler = createChildReconciler(
true,
);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
export const mountChildFibersImpl: ChildReconciler = createChildReconciler(
false,
);

export function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// This indirection only exists so we can reset `thenableState` at the end.
// It should get inlined by Closure.
thenableIndexCounter = 0;
const firstChildFiber = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
thenableState = null;
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
// set at the beginning.
return firstChildFiber;
}

export function mountChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// This indirection only exists so we can reset `thenableState` at the end.
// It should get inlined by Closure.
thenableIndexCounter = 0;
const firstChildFiber = mountChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
thenableState = null;
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
// set at the beginning.
return firstChildFiber;
}

export function resetChildReconcilerOnUnwind(): void {
// On unwind, clear any pending thenables that were used.
thenableState = null;
thenableIndexCounter = 0;
}

export function cloneChildFibers(
current: Fiber | null,
Expand Down
117 changes: 112 additions & 5 deletions packages/react-reconciler/src/ReactChildFiber.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
*/

import type {ReactElement} from 'shared/ReactElementType';
import type {ReactPortal} from 'shared/ReactTypes';
import type {ReactPortal, Thenable} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.old';
import type {ThenableState} from './ReactFiberThenable.old';

import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import {
Expand All @@ -25,6 +26,8 @@ import {
REACT_FRAGMENT_TYPE,
REACT_PORTAL_TYPE,
REACT_LAZY_TYPE,
REACT_CONTEXT_TYPE,
REACT_SERVER_CONTEXT_TYPE,
} from 'shared/ReactSymbols';
import {ClassComponent, HostText, HostPortal, Fragment} from './ReactWorkTags';
import isArray from 'shared/isArray';
Expand All @@ -44,6 +47,11 @@ import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.old';
import {StrictLegacyMode} from './ReactTypeOfMode';
import {getIsHydrating} from './ReactFiberHydrationContext.old';
import {pushTreeFork} from './ReactFiberTreeContext.old';
import {createThenableState, trackUsedThenable} from './ReactFiberThenable.old';

// This tracks the thenables that are unwrapped during reconcilation.
let thenableState: ThenableState | null = null;
let thenableIndexCounter: number = 0;

let didWarnAboutMaps;
let didWarnAboutGenerators;
Expand Down Expand Up @@ -98,6 +106,48 @@ if (__DEV__) {
};
}

function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
// Promises are a valid React node type. When the reconciler encounters a
// promise in a child position, it unwraps it using the `use` algorithm: by
// throwing an exception to unwind the stack, then replaying the begin phase
// once the promise resolves.
//
// 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
// reconcilation happens deep within the begin phase, and depending on the
// type of work, not always at the end. We should consider as an
// future improvement.
//
// Keep unwrapping the value until we reach a non-Usable type.
//
// e.g. Usable<Usable<Usable<T>>> should resolve to T
while (maybeUsable !== null && maybeUsable !== undefined) {
if (typeof maybeUsable.then === 'function') {
// This is a thenable
const thenable: Thenable<any> = (maybeUsable: any);
const index = thenableIndexCounter;
thenableIndexCounter += 1;

if (thenableState === null) {
thenableState = createThenableState();
}
maybeUsable = trackUsedThenable(thenableState, thenable, index);
continue;
} else if (
maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
// const context: ReactContext<mixed> = (maybeUsable: any);
// maybeUsable = readContext(context);
// continue;
}
break;
}
return maybeUsable;
}

function coerceRef(
returnFiber: Fiber,
current: Fiber | null,
Expand Down Expand Up @@ -502,6 +552,8 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);

if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
Expand Down Expand Up @@ -576,6 +628,7 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
lanes: Lanes,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);

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

Expand Down Expand Up @@ -642,6 +695,8 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);

if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
Expand Down Expand Up @@ -1256,12 +1311,14 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
// This API will tag the children with the side-effect of the reconciliation
// itself. They will be added to the side-effect list as we pass through the
// children and the parent.
function reconcileChildFibers(
function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);

// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
Expand Down Expand Up @@ -1357,13 +1414,63 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

return reconcileChildFibers;
return reconcileChildFibersImpl;
}

export const reconcileChildFibers: ChildReconciler = createChildReconciler(
export const reconcileChildFibersImpl: ChildReconciler = createChildReconciler(
true,
);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
export const mountChildFibersImpl: ChildReconciler = createChildReconciler(
false,
);

export function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// This indirection only exists so we can reset `thenableState` at the end.
// It should get inlined by Closure.
thenableIndexCounter = 0;
const firstChildFiber = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
thenableState = null;
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
// set at the beginning.
return firstChildFiber;
}

export function mountChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// This indirection only exists so we can reset `thenableState` at the end.
// It should get inlined by Closure.
thenableIndexCounter = 0;
const firstChildFiber = mountChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
thenableState = null;
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
// set at the beginning.
return firstChildFiber;
}

export function resetChildReconcilerOnUnwind(): void {
// On unwind, clear any pending thenables that were used.
thenableState = null;
thenableIndexCounter = 0;
}

export function cloneChildFibers(
current: Fiber | null,
Expand Down
Loading

0 comments on commit ca01d51

Please sign in to comment.