Skip to content

Commit 63b51d3

Browse files
committed
Support Context as renderable node
Like promises, this adds support for Context as a React node. 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.
1 parent 324cf1f commit 63b51d3

File tree

7 files changed

+243
-32
lines changed

7 files changed

+243
-32
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5345,6 +5345,34 @@ describe('ReactDOMFizzServer', () => {
53455345

53465346
expect(getVisibleChildren(container)).toEqual('Hi');
53475347
});
5348+
5349+
it('context as node', async () => {
5350+
const Context = React.createContext('Hi');
5351+
await act(async () => {
5352+
const {pipe} = renderToPipeableStream(Context);
5353+
pipe(writable);
5354+
});
5355+
expect(getVisibleChildren(container)).toEqual('Hi');
5356+
});
5357+
5358+
it('recursive Usable as node', async () => {
5359+
const Context = React.createContext('Hi');
5360+
const promiseForContext = Promise.resolve(Context);
5361+
await act(async () => {
5362+
const {pipe} = renderToPipeableStream(promiseForContext);
5363+
pipe(writable);
5364+
});
5365+
5366+
// TODO: The `act` implementation in this file doesn't unwrap microtasks
5367+
// automatically. We can't use the same `act` we use for Fiber tests
5368+
// because that relies on the mock Scheduler. Doesn't affect any public
5369+
// API but we might want to fix this for our own internal tests.
5370+
await act(async () => {
5371+
await promiseForContext;
5372+
});
5373+
5374+
expect(getVisibleChildren(container)).toEqual('Hi');
5375+
});
53485376
});
53495377

53505378
describe('useEvent', () => {

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

Lines changed: 30 additions & 11 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,7 +107,11 @@ if (__DEV__) {
106107
};
107108
}
108109

109-
function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
110+
function transparentlyUnwrapPossiblyUsableValue(
111+
returnFiber: Fiber,
112+
maybeUsable: Object,
113+
lanes: Lanes,
114+
): any {
110115
// Usables are a valid React node type. When React encounters a Usable in a
111116
// child position, it unwraps it using the same algorithm as `use`. For
112117
// example, for promises, React will throw an exception to unwind the stack,
@@ -116,7 +121,6 @@ function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
116121
// value until it reaches a non-Usable type.
117122
//
118123
// e.g. Usable<Usable<Usable<T>>> should resolve to T
119-
//
120124
// The structure is a bit unfortunate. Ideally, we shouldn't need to replay
121125
// the entire begin phase of the parent fiber in order to reconcile the
122126
// children again. This would require a somewhat significant refactor, because
@@ -139,10 +143,9 @@ function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
139143
maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
140144
maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
141145
) {
142-
// TODO: Implement Context as child type.
143-
// const context: ReactContext<mixed> = (maybeUsable: any);
144-
// maybeUsable = readContext(context);
145-
// continue;
146+
const context: ReactContext<mixed> = (maybeUsable: any);
147+
maybeUsable = readContextDuringReconcilation(returnFiber, context, lanes);
148+
continue;
146149
}
147150
break;
148151
}
@@ -553,7 +556,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
553556
newChild: any,
554557
lanes: Lanes,
555558
): Fiber | null {
556-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
559+
newChild = transparentlyUnwrapPossiblyUsableValue(
560+
returnFiber,
561+
newChild,
562+
lanes,
563+
);
557564

558565
if (
559566
(typeof newChild === 'string' && newChild !== '') ||
@@ -629,7 +636,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
629636
lanes: Lanes,
630637
): Fiber | null {
631638
// Update the fiber if the keys match, otherwise return null.
632-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
639+
newChild = transparentlyUnwrapPossiblyUsableValue(
640+
returnFiber,
641+
newChild,
642+
lanes,
643+
);
633644

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

@@ -696,7 +707,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
696707
newChild: any,
697708
lanes: Lanes,
698709
): Fiber | null {
699-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
710+
newChild = transparentlyUnwrapPossiblyUsableValue(
711+
returnFiber,
712+
newChild,
713+
lanes,
714+
);
700715

701716
if (
702717
(typeof newChild === 'string' && newChild !== '') ||
@@ -1318,7 +1333,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
13181333
newChild: any,
13191334
lanes: Lanes,
13201335
): Fiber | null {
1321-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
1336+
newChild = transparentlyUnwrapPossiblyUsableValue(
1337+
returnFiber,
1338+
newChild,
1339+
lanes,
1340+
);
13221341

13231342
// This function is not recursive.
13241343
// 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: 30 additions & 11 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,7 +107,11 @@ if (__DEV__) {
106107
};
107108
}
108109

109-
function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
110+
function transparentlyUnwrapPossiblyUsableValue(
111+
returnFiber: Fiber,
112+
maybeUsable: Object,
113+
lanes: Lanes,
114+
): any {
110115
// Usables are a valid React node type. When React encounters a Usable in a
111116
// child position, it unwraps it using the same algorithm as `use`. For
112117
// example, for promises, React will throw an exception to unwind the stack,
@@ -116,7 +121,6 @@ function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
116121
// value until it reaches a non-Usable type.
117122
//
118123
// e.g. Usable<Usable<Usable<T>>> should resolve to T
119-
//
120124
// The structure is a bit unfortunate. Ideally, we shouldn't need to replay
121125
// the entire begin phase of the parent fiber in order to reconcile the
122126
// children again. This would require a somewhat significant refactor, because
@@ -139,10 +143,9 @@ function transparentlyUnwrapPossiblyUsableValue(maybeUsable: Object): any {
139143
maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
140144
maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
141145
) {
142-
// TODO: Implement Context as child type.
143-
// const context: ReactContext<mixed> = (maybeUsable: any);
144-
// maybeUsable = readContext(context);
145-
// continue;
146+
const context: ReactContext<mixed> = (maybeUsable: any);
147+
maybeUsable = readContextDuringReconcilation(returnFiber, context, lanes);
148+
continue;
146149
}
147150
break;
148151
}
@@ -553,7 +556,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
553556
newChild: any,
554557
lanes: Lanes,
555558
): Fiber | null {
556-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
559+
newChild = transparentlyUnwrapPossiblyUsableValue(
560+
returnFiber,
561+
newChild,
562+
lanes,
563+
);
557564

558565
if (
559566
(typeof newChild === 'string' && newChild !== '') ||
@@ -629,7 +636,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
629636
lanes: Lanes,
630637
): Fiber | null {
631638
// Update the fiber if the keys match, otherwise return null.
632-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
639+
newChild = transparentlyUnwrapPossiblyUsableValue(
640+
returnFiber,
641+
newChild,
642+
lanes,
643+
);
633644

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

@@ -696,7 +707,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
696707
newChild: any,
697708
lanes: Lanes,
698709
): Fiber | null {
699-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
710+
newChild = transparentlyUnwrapPossiblyUsableValue(
711+
returnFiber,
712+
newChild,
713+
lanes,
714+
);
700715

701716
if (
702717
(typeof newChild === 'string' && newChild !== '') ||
@@ -1318,7 +1333,11 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
13181333
newChild: any,
13191334
lanes: Lanes,
13201335
): Fiber | null {
1321-
newChild = transparentlyUnwrapPossiblyUsableValue(newChild);
1336+
newChild = transparentlyUnwrapPossiblyUsableValue(
1337+
returnFiber,
1338+
newChild,
1339+
lanes,
1340+
);
13221341

13231342
// This function is not recursive.
13241343
// 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)