Skip to content

Commit

Permalink
Support Context as React node (Fiber)
Browse files Browse the repository at this point in the history
Like promises, this adds support for Context as a React node. The idea
is that any Usable type can also be rendered as a child, and React will
transparently unwrap it before it is reconciled.

In this initial implementation, the context dependency is added to the
parent of child node. This allows the parent to re-reconcile its
children when the context updates, so that it can delete the old node if
the identity of the child has changed (i.e. if the key or type of an
element has changed). But it also means that the parent will replay
its entire begin phase. Ideally React would delete the old node and
mount the new node without reconciling all the children. I'll leave this
for a future optimization.

Currently only supports Fiber. Will add support for Fizz and Flight
as a follow up.
  • Loading branch information
acdlite committed Nov 6, 2022
1 parent 53f5329 commit 747bac7
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 40 deletions.
55 changes: 38 additions & 17 deletions packages/react-reconciler/src/ReactChildFiber.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import type {ReactElement} from 'shared/ReactElementType';
import type {ReactPortal, Thenable} from 'shared/ReactTypes';
import type {ReactPortal, Thenable, ReactContext} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.new';
import type {ThenableState} from './ReactFiberThenable.new';
Expand Down Expand Up @@ -48,6 +48,7 @@ import {StrictLegacyMode} from './ReactTypeOfMode';
import {getIsHydrating} from './ReactFiberHydrationContext.new';
import {pushTreeFork} from './ReactFiberTreeContext.new';
import {createThenableState, trackUsedThenable} from './ReactFiberThenable.new';
import {readContextDuringReconcilation} from './ReactFiberNewContext.new';

// This tracks the thenables that are unwrapped during reconcilation.
let thenableState: ThenableState | null = null;
Expand Down Expand Up @@ -106,20 +107,25 @@ 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.
function transparentlyUnwrapPossiblyUsableValue(
returnFiber: Fiber,
maybeUsable: Object,
lanes: Lanes,
): any {
// Usables are a valid React node type. When the reconciler 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 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.
// 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.
// A difference from `use` is that the reconciler will keep unwrapping the
// value until it reaches a non-Usable type.
//
// e.g. Usable<Usable<Usable<T>>> should resolve to T
while (maybeUsable !== null && maybeUsable !== undefined) {
Expand All @@ -138,10 +144,9 @@ function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
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;
const context: ReactContext<mixed> = (maybeUsable: any);
maybeUsable = readContextDuringReconcilation(returnFiber, context, lanes);
continue;
}
break;
}
Expand Down Expand Up @@ -552,7 +557,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
newChild = transparentlyUnwrapPossiblyUsableValue(
returnFiber,
newChild,
lanes,
);

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

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

Expand Down Expand Up @@ -695,7 +708,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
newChild = transparentlyUnwrapPossiblyUsableValue(
returnFiber,
newChild,
lanes,
);

if (
(typeof newChild === 'string' && newChild !== '') ||
Expand Down Expand Up @@ -1317,7 +1334,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
newChild = transparentlyUnwrapPossiblyUsableValue(
returnFiber,
newChild,
lanes,
);

// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
Expand Down
55 changes: 38 additions & 17 deletions packages/react-reconciler/src/ReactChildFiber.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import type {ReactElement} from 'shared/ReactElementType';
import type {ReactPortal, Thenable} from 'shared/ReactTypes';
import type {ReactPortal, Thenable, ReactContext} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.old';
import type {ThenableState} from './ReactFiberThenable.old';
Expand Down Expand Up @@ -48,6 +48,7 @@ import {StrictLegacyMode} from './ReactTypeOfMode';
import {getIsHydrating} from './ReactFiberHydrationContext.old';
import {pushTreeFork} from './ReactFiberTreeContext.old';
import {createThenableState, trackUsedThenable} from './ReactFiberThenable.old';
import {readContextDuringReconcilation} from './ReactFiberNewContext.old';

// This tracks the thenables that are unwrapped during reconcilation.
let thenableState: ThenableState | null = null;
Expand Down Expand Up @@ -106,20 +107,25 @@ 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.
function transparentlyUnwrapPossiblyUsableValue(
returnFiber: Fiber,
maybeUsable: Object,
lanes: Lanes,
): any {
// Usables are a valid React node type. When the reconciler 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 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.
// 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.
// A difference from `use` is that the reconciler will keep unwrapping the
// value until it reaches a non-Usable type.
//
// e.g. Usable<Usable<Usable<T>>> should resolve to T
while (maybeUsable !== null && maybeUsable !== undefined) {
Expand All @@ -138,10 +144,9 @@ function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
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;
const context: ReactContext<mixed> = (maybeUsable: any);
maybeUsable = readContextDuringReconcilation(returnFiber, context, lanes);
continue;
}
break;
}
Expand Down Expand Up @@ -552,7 +557,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
newChild = transparentlyUnwrapPossiblyUsableValue(
returnFiber,
newChild,
lanes,
);

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

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

Expand Down Expand Up @@ -695,7 +708,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
newChild = transparentlyUnwrapPossiblyUsableValue(
returnFiber,
newChild,
lanes,
);

if (
(typeof newChild === 'string' && newChild !== '') ||
Expand Down Expand Up @@ -1317,7 +1334,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild: any,
lanes: Lanes,
): Fiber | null {
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
newChild = transparentlyUnwrapPossiblyUsableValue(
returnFiber,
newChild,
lanes,
);

// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
Expand Down
23 changes: 20 additions & 3 deletions packages/react-reconciler/src/ReactFiberNewContext.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,24 @@ export function readContext<T>(context: ReactContext<T>): T {
);
}
}
return readContextForConsumer(currentlyRenderingFiber, context);
}

export function readContextDuringReconcilation<T>(
consumer: Fiber,
context: ReactContext<T>,
renderLanes: Lanes,
): T {
if (currentlyRenderingFiber === null) {
prepareToReadContext(consumer, renderLanes);
}
return readContextForConsumer(consumer, context);
}

function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
Expand All @@ -679,7 +696,7 @@ export function readContext<T>(context: ReactContext<T>): T {
};

if (lastContextDependency === null) {
if (currentlyRenderingFiber === null) {
if (consumer === null) {
throw new Error(
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
Expand All @@ -690,12 +707,12 @@ export function readContext<T>(context: ReactContext<T>): T {

// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
currentlyRenderingFiber.dependencies = {
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
currentlyRenderingFiber.flags |= NeedsPropagation;
consumer.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
Expand Down
23 changes: 20 additions & 3 deletions packages/react-reconciler/src/ReactFiberNewContext.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,24 @@ export function readContext<T>(context: ReactContext<T>): T {
);
}
}
return readContextForConsumer(currentlyRenderingFiber, context);
}

export function readContextDuringReconcilation<T>(
consumer: Fiber,
context: ReactContext<T>,
renderLanes: Lanes,
): T {
if (currentlyRenderingFiber === null) {
prepareToReadContext(consumer, renderLanes);
}
return readContextForConsumer(consumer, context);
}

function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>,
): T {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
Expand All @@ -679,7 +696,7 @@ export function readContext<T>(context: ReactContext<T>): T {
};

if (lastContextDependency === null) {
if (currentlyRenderingFiber === null) {
if (consumer === null) {
throw new Error(
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
Expand All @@ -690,12 +707,12 @@ export function readContext<T>(context: ReactContext<T>): T {

// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
currentlyRenderingFiber.dependencies = {
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
currentlyRenderingFiber.flags |= NeedsPropagation;
consumer.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
Expand Down
Loading

0 comments on commit 747bac7

Please sign in to comment.