Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Promise as a renderable node #25634

Merged
merged 2 commits into from
Mar 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5402,6 +5402,24 @@ describe('ReactDOMFizzServer', () => {
});
expect(getVisibleChildren(container)).toEqual('Hi');
});

it('promise as node', async () => {
const promise = Promise.resolve('Hi');
await act(async () => {
const {pipe} = renderToPipeableStream(promise);
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.
await act(async () => {
await promise;
});

expect(getVisibleChildren(container)).toEqual('Hi');
});
});

describe('useEffectEvent', () => {
Expand Down
139 changes: 136 additions & 3 deletions packages/react-reconciler/src/ReactChildFiber.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';
import type {ThenableState} from './ReactFiberThenable';

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 @@ -41,6 +44,11 @@ import {
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading';
import {getIsHydrating} from './ReactFiberHydrationContext';
import {pushTreeFork} from './ReactFiberTreeContext';
import {createThenableState, trackUsedThenable} from './ReactFiberThenable';

// 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 @@ -99,6 +107,15 @@ function isReactClass(type: any) {
return type.prototype && type.prototype.isReactComponent;
}

function unwrapThenable<T>(thenable: Thenable<T>): T {
const index = thenableIndexCounter;
thenableIndexCounter += 1;
if (thenableState === null) {
thenableState = createThenableState();
}
return trackUsedThenable(thenableState, thenable, index);
}

function coerceRef(
returnFiber: Fiber,
current: Fiber | null,
Expand Down Expand Up @@ -551,6 +568,21 @@ function createChildReconciler(
return created;
}

// Usable node types
//
// Unwrap the inner value and recursively call this function again.
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
return createChild(returnFiber, unwrapThenable(thenable), lanes);
}

if (
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
}

throwOnInvalidObjectType(returnFiber, newChild);
}

Expand All @@ -570,7 +602,6 @@ function createChildReconciler(
lanes: Lanes,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.

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

if (
Expand Down Expand Up @@ -617,6 +648,26 @@ function createChildReconciler(
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
}

// Usable node types
//
// Unwrap the inner value and recursively call this function again.
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
return updateSlot(
returnFiber,
oldFiber,
unwrapThenable(thenable),
lanes,
);
}

if (
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
}

throwOnInvalidObjectType(returnFiber, newChild);
}

Expand Down Expand Up @@ -679,6 +730,27 @@ function createChildReconciler(
return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
}

// Usable node types
//
// Unwrap the inner value and recursively call this function again.
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
return updateFromMap(
existingChildren,
returnFiber,
newIdx,
unwrapThenable(thenable),
lanes,
);
}

if (
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
}

throwOnInvalidObjectType(returnFiber, newChild);
}

Expand Down Expand Up @@ -1250,7 +1322,7 @@ function createChildReconciler(
// 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,
Expand All @@ -1264,6 +1336,7 @@ function createChildReconciler(
// Handle top level unkeyed fragments as if they were arrays.
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
// We treat the ambiguous cases above the same.
// TODO: Let's use recursion like we do for Usable nodes?
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
Expand Down Expand Up @@ -1324,6 +1397,39 @@ function createChildReconciler(
);
}

// Usables are a valid React node type. When React encounters a Usable in
// a child position, it unwraps it using the same algorithm as `use`. For
// example, for promises, React will throw an exception to unwind the
// stack, then replay the component once the promise resolves.
//
// A difference from `use` is that React will keep unwrapping the value
// until it reaches a non-Usable type.
//
// e.g. Usable<Usable<Usable<T>>> should resolve to T
//
// 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.
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
unwrapThenable(thenable),
lanes,
);
}

if (
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
}

throwOnInvalidObjectType(returnFiber, newChild);
}

Expand Down Expand Up @@ -1351,13 +1457,40 @@ function createChildReconciler(
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

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;
}

return reconcileChildFibers;
}

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

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

export function cloneChildFibers(
current: Fiber | null,
workInProgress: Fiber,
Expand Down
18 changes: 10 additions & 8 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ import {
getShellBoundary,
} from './ReactFiberSuspenseContext';
import {resolveDefaultProps} from './ReactFiberLazyComponent';
import {resetChildReconcilerOnUnwind} from './ReactChildFiber';

const ceil = Math.ceil;

Expand Down Expand Up @@ -1766,6 +1767,7 @@ function resetSuspendedWorkLoopOnUnwind() {
// Reset module-level state that was set during the render phase.
resetContextDependencies();
resetHooksOnUnwind();
resetChildReconcilerOnUnwind();
}

function handleThrow(root: FiberRoot, thrownValue: any): void {
Expand Down Expand Up @@ -2423,14 +2425,14 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
break;
}
default: {
if (__DEV__) {
console.error(
'Unexpected type of work: %s, Currently only function ' +
'components are replayed after suspending. This is a bug in React.',
unitOfWork.tag,
);
}
resetSuspendedWorkLoopOnUnwind();
// Other types besides function components are reset completely before
// being replayed. Currently this only happens when a Usable type is
// reconciled — the reconciler will suspend.
//
// We reset the fiber back to its original state; however, this isn't
// a full "unwind" because we're going to reuse the promises that were
// reconciled previously. So it's intentional that we don't call
// resetSuspendedWorkLoopOnUnwind here.
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
unitOfWork = workInProgress = resetWorkInProgress(
unitOfWork,
Expand Down
Loading