Skip to content

Commit 747bac7

Browse files
committed
Support Context as React node (Fiber)
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.
1 parent 53f5329 commit 747bac7

File tree

5 files changed

+228
-40
lines changed

5 files changed

+228
-40
lines changed

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

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import type {ReactElement} from 'shared/ReactElementType';
11-
import type {ReactPortal, Thenable} from 'shared/ReactTypes';
11+
import type {ReactPortal, Thenable, ReactContext} from 'shared/ReactTypes';
1212
import type {Fiber} from './ReactInternalTypes';
1313
import type {Lanes} from './ReactFiberLane.new';
1414
import type {ThenableState} from './ReactFiberThenable.new';
@@ -48,6 +48,7 @@ import {StrictLegacyMode} from './ReactTypeOfMode';
4848
import {getIsHydrating} from './ReactFiberHydrationContext.new';
4949
import {pushTreeFork} from './ReactFiberTreeContext.new';
5050
import {createThenableState, trackUsedThenable} from './ReactFiberThenable.new';
51+
import {readContextDuringReconcilation} from './ReactFiberNewContext.new';
5152

5253
// This tracks the thenables that are unwrapped during reconcilation.
5354
let thenableState: ThenableState | null = null;
@@ -106,20 +107,25 @@ if (__DEV__) {
106107
};
107108
}
108109

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.
110+
function transparentlyUnwrapPossiblyUsableValue(
111+
returnFiber: Fiber,
112+
maybeUsable: Object,
113+
lanes: Lanes,
114+
): any {
115+
// Usables are a valid React node type. When the reconciler encounters a
116+
// Usable in a child position, it unwraps it using the same algorithm as
117+
// `use`. For example, for promises, React will throw an exception to unwind
118+
// the stack, then replay the begin phase once the promise resolves.
114119
//
115120
// The structure is a bit unfortunate. Ideally, we shouldn't need to replay
116121
// the entire begin phase of the parent fiber in order to reconcile the
117122
// children again. This would require a somewhat significant refactor, because
118123
// 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.
124+
// type of work, not always at the end. We should consider as an future
125+
// improvement.
121126
//
122-
// Keep unwrapping the value until we reach a non-Usable type.
127+
// A difference from `use` is that the reconciler will keep unwrapping the
128+
// value until it reaches a non-Usable type.
123129
//
124130
// e.g. Usable<Usable<Usable<T>>> should resolve to T
125131
while (maybeUsable !== null && maybeUsable !== undefined) {
@@ -138,10 +144,9 @@ function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
138144
maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
139145
maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
140146
) {
141-
// TODO: Implement Context as child type.
142-
// const context: ReactContext<mixed> = (maybeUsable: any);
143-
// maybeUsable = readContext(context);
144-
// continue;
147+
const context: ReactContext<mixed> = (maybeUsable: any);
148+
maybeUsable = readContextDuringReconcilation(returnFiber, context, lanes);
149+
continue;
145150
}
146151
break;
147152
}
@@ -552,7 +557,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
552557
newChild: any,
553558
lanes: Lanes,
554559
): Fiber | null {
555-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
560+
newChild = transparentlyUnwrapPossiblyUsableValue(
561+
returnFiber,
562+
newChild,
563+
lanes,
564+
);
556565

557566
if (
558567
(typeof newChild === 'string' && newChild !== '') ||
@@ -628,7 +637,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
628637
lanes: Lanes,
629638
): Fiber | null {
630639
// Update the fiber if the keys match, otherwise return null.
631-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
640+
newChild = transparentlyUnwrapPossiblyUsableValue(
641+
returnFiber,
642+
newChild,
643+
lanes,
644+
);
632645

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

@@ -695,7 +708,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
695708
newChild: any,
696709
lanes: Lanes,
697710
): Fiber | null {
698-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
711+
newChild = transparentlyUnwrapPossiblyUsableValue(
712+
returnFiber,
713+
newChild,
714+
lanes,
715+
);
699716

700717
if (
701718
(typeof newChild === 'string' && newChild !== '') ||
@@ -1317,7 +1334,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
13171334
newChild: any,
13181335
lanes: Lanes,
13191336
): Fiber | null {
1320-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
1337+
newChild = transparentlyUnwrapPossiblyUsableValue(
1338+
returnFiber,
1339+
newChild,
1340+
lanes,
1341+
);
13211342

13221343
// This function is not recursive.
13231344
// If the top level item is an array, we treat it as a set of children,

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

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import type {ReactElement} from 'shared/ReactElementType';
11-
import type {ReactPortal, Thenable} from 'shared/ReactTypes';
11+
import type {ReactPortal, Thenable, ReactContext} from 'shared/ReactTypes';
1212
import type {Fiber} from './ReactInternalTypes';
1313
import type {Lanes} from './ReactFiberLane.old';
1414
import type {ThenableState} from './ReactFiberThenable.old';
@@ -48,6 +48,7 @@ import {StrictLegacyMode} from './ReactTypeOfMode';
4848
import {getIsHydrating} from './ReactFiberHydrationContext.old';
4949
import {pushTreeFork} from './ReactFiberTreeContext.old';
5050
import {createThenableState, trackUsedThenable} from './ReactFiberThenable.old';
51+
import {readContextDuringReconcilation} from './ReactFiberNewContext.old';
5152

5253
// This tracks the thenables that are unwrapped during reconcilation.
5354
let thenableState: ThenableState | null = null;
@@ -106,20 +107,25 @@ if (__DEV__) {
106107
};
107108
}
108109

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.
110+
function transparentlyUnwrapPossiblyUsableValue(
111+
returnFiber: Fiber,
112+
maybeUsable: Object,
113+
lanes: Lanes,
114+
): any {
115+
// Usables are a valid React node type. When the reconciler encounters a
116+
// Usable in a child position, it unwraps it using the same algorithm as
117+
// `use`. For example, for promises, React will throw an exception to unwind
118+
// the stack, then replay the begin phase once the promise resolves.
114119
//
115120
// The structure is a bit unfortunate. Ideally, we shouldn't need to replay
116121
// the entire begin phase of the parent fiber in order to reconcile the
117122
// children again. This would require a somewhat significant refactor, because
118123
// 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.
124+
// type of work, not always at the end. We should consider as an future
125+
// improvement.
121126
//
122-
// Keep unwrapping the value until we reach a non-Usable type.
127+
// A difference from `use` is that the reconciler will keep unwrapping the
128+
// value until it reaches a non-Usable type.
123129
//
124130
// e.g. Usable<Usable<Usable<T>>> should resolve to T
125131
while (maybeUsable !== null && maybeUsable !== undefined) {
@@ -138,10 +144,9 @@ function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
138144
maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
139145
maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
140146
) {
141-
// TODO: Implement Context as child type.
142-
// const context: ReactContext<mixed> = (maybeUsable: any);
143-
// maybeUsable = readContext(context);
144-
// continue;
147+
const context: ReactContext<mixed> = (maybeUsable: any);
148+
maybeUsable = readContextDuringReconcilation(returnFiber, context, lanes);
149+
continue;
145150
}
146151
break;
147152
}
@@ -552,7 +557,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
552557
newChild: any,
553558
lanes: Lanes,
554559
): Fiber | null {
555-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
560+
newChild = transparentlyUnwrapPossiblyUsableValue(
561+
returnFiber,
562+
newChild,
563+
lanes,
564+
);
556565

557566
if (
558567
(typeof newChild === 'string' && newChild !== '') ||
@@ -628,7 +637,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
628637
lanes: Lanes,
629638
): Fiber | null {
630639
// Update the fiber if the keys match, otherwise return null.
631-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
640+
newChild = transparentlyUnwrapPossiblyUsableValue(
641+
returnFiber,
642+
newChild,
643+
lanes,
644+
);
632645

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

@@ -695,7 +708,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
695708
newChild: any,
696709
lanes: Lanes,
697710
): Fiber | null {
698-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
711+
newChild = transparentlyUnwrapPossiblyUsableValue(
712+
returnFiber,
713+
newChild,
714+
lanes,
715+
);
699716

700717
if (
701718
(typeof newChild === 'string' && newChild !== '') ||
@@ -1317,7 +1334,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
13171334
newChild: any,
13181335
lanes: Lanes,
13191336
): Fiber | null {
1320-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
1337+
newChild = transparentlyUnwrapPossiblyUsableValue(
1338+
returnFiber,
1339+
newChild,
1340+
lanes,
1341+
);
13211342

13221343
// This function is not recursive.
13231344
// If the top level item is an array, we treat it as a set of children,

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,24 @@ export function readContext<T>(context: ReactContext<T>): T {
664664
);
665665
}
666666
}
667+
return readContextForConsumer(currentlyRenderingFiber, context);
668+
}
667669

670+
export function readContextDuringReconcilation<T>(
671+
consumer: Fiber,
672+
context: ReactContext<T>,
673+
renderLanes: Lanes,
674+
): T {
675+
if (currentlyRenderingFiber === null) {
676+
prepareToReadContext(consumer, renderLanes);
677+
}
678+
return readContextForConsumer(consumer, context);
679+
}
680+
681+
function readContextForConsumer<T>(
682+
consumer: Fiber | null,
683+
context: ReactContext<T>,
684+
): T {
668685
const value = isPrimaryRenderer
669686
? context._currentValue
670687
: context._currentValue2;
@@ -679,7 +696,7 @@ export function readContext<T>(context: ReactContext<T>): T {
679696
};
680697

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

691708
// This is the first dependency for this component. Create a new list.
692709
lastContextDependency = contextItem;
693-
currentlyRenderingFiber.dependencies = {
710+
consumer.dependencies = {
694711
lanes: NoLanes,
695712
firstContext: contextItem,
696713
};
697714
if (enableLazyContextPropagation) {
698-
currentlyRenderingFiber.flags |= NeedsPropagation;
715+
consumer.flags |= NeedsPropagation;
699716
}
700717
} else {
701718
// Append a new context item.

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,24 @@ export function readContext<T>(context: ReactContext<T>): T {
664664
);
665665
}
666666
}
667+
return readContextForConsumer(currentlyRenderingFiber, context);
668+
}
667669

670+
export function readContextDuringReconcilation<T>(
671+
consumer: Fiber,
672+
context: ReactContext<T>,
673+
renderLanes: Lanes,
674+
): T {
675+
if (currentlyRenderingFiber === null) {
676+
prepareToReadContext(consumer, renderLanes);
677+
}
678+
return readContextForConsumer(consumer, context);
679+
}
680+
681+
function readContextForConsumer<T>(
682+
consumer: Fiber | null,
683+
context: ReactContext<T>,
684+
): T {
668685
const value = isPrimaryRenderer
669686
? context._currentValue
670687
: context._currentValue2;
@@ -679,7 +696,7 @@ export function readContext<T>(context: ReactContext<T>): T {
679696
};
680697

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

691708
// This is the first dependency for this component. Create a new list.
692709
lastContextDependency = contextItem;
693-
currentlyRenderingFiber.dependencies = {
710+
consumer.dependencies = {
694711
lanes: NoLanes,
695712
firstContext: contextItem,
696713
};
697714
if (enableLazyContextPropagation) {
698-
currentlyRenderingFiber.flags |= NeedsPropagation;
715+
consumer.flags |= NeedsPropagation;
699716
}
700717
} else {
701718
// Append a new context item.

0 commit comments

Comments
 (0)