Skip to content

Commit a665a00

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 e593439 commit a665a00

File tree

7 files changed

+247
-17
lines changed

7 files changed

+247
-17
lines changed

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

+28
Original file line numberDiff line numberDiff line change
@@ -5375,6 +5375,34 @@ describe('ReactDOMFizzServer', () => {
53755375

53765376
expect(getVisibleChildren(container)).toEqual('Hi');
53775377
});
5378+
5379+
it('context as node', async () => {
5380+
const Context = React.createContext('Hi');
5381+
await act(async () => {
5382+
const {pipe} = renderToPipeableStream(Context);
5383+
pipe(writable);
5384+
});
5385+
expect(getVisibleChildren(container)).toEqual('Hi');
5386+
});
5387+
5388+
it('recursive Usable as node', async () => {
5389+
const Context = React.createContext('Hi');
5390+
const promiseForContext = Promise.resolve(Context);
5391+
await act(async () => {
5392+
const {pipe} = renderToPipeableStream(promiseForContext);
5393+
pipe(writable);
5394+
});
5395+
5396+
// TODO: The `act` implementation in this file doesn't unwrap microtasks
5397+
// automatically. We can't use the same `act` we use for Fiber tests
5398+
// because that relies on the mock Scheduler. Doesn't affect any public
5399+
// API but we might want to fix this for our own internal tests.
5400+
await act(async () => {
5401+
await promiseForContext;
5402+
});
5403+
5404+
expect(getVisibleChildren(container)).toEqual('Hi');
5405+
});
53785406
});
53795407

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

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

+30-5
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';
@@ -47,6 +47,7 @@ import {StrictLegacyMode} from './ReactTypeOfMode';
4747
import {getIsHydrating} from './ReactFiberHydrationContext.new';
4848
import {pushTreeFork} from './ReactFiberTreeContext.new';
4949
import {createThenableState, trackUsedThenable} from './ReactFiberThenable.new';
50+
import {readContextDuringReconcilation} from './ReactFiberNewContext.new';
5051

5152
// This tracks the thenables that are unwrapped during reconcilation.
5253
let thenableState: ThenableState | null = null;
@@ -594,7 +595,12 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
594595
newChild.$$typeof === REACT_CONTEXT_TYPE ||
595596
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
596597
) {
597-
// TODO: Implement Context as child type.
598+
const context: ReactContext<mixed> = (newChild: any);
599+
return createChild(
600+
returnFiber,
601+
readContextDuringReconcilation(returnFiber, context, lanes),
602+
lanes,
603+
);
598604
}
599605

600606
throwOnInvalidObjectType(returnFiber, newChild);
@@ -679,7 +685,13 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
679685
newChild.$$typeof === REACT_CONTEXT_TYPE ||
680686
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
681687
) {
682-
// TODO: Implement Context as child type.
688+
const context: ReactContext<mixed> = (newChild: any);
689+
return updateSlot(
690+
returnFiber,
691+
oldFiber,
692+
readContextDuringReconcilation(returnFiber, context, lanes),
693+
lanes,
694+
);
683695
}
684696

685697
throwOnInvalidObjectType(returnFiber, newChild);
@@ -762,7 +774,14 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
762774
newChild.$$typeof === REACT_CONTEXT_TYPE ||
763775
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
764776
) {
765-
// TODO: Implement Context as child type.
777+
const context: ReactContext<mixed> = (newChild: any);
778+
return updateFromMap(
779+
existingChildren,
780+
returnFiber,
781+
newIdx,
782+
readContextDuringReconcilation(returnFiber, context, lanes),
783+
lanes,
784+
);
766785
}
767786

768787
throwOnInvalidObjectType(returnFiber, newChild);
@@ -1441,7 +1460,13 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
14411460
newChild.$$typeof === REACT_CONTEXT_TYPE ||
14421461
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
14431462
) {
1444-
// TODO: Implement Context as child type.
1463+
const context: ReactContext<mixed> = (newChild: any);
1464+
return reconcileChildFibersImpl(
1465+
returnFiber,
1466+
currentFirstChild,
1467+
readContextDuringReconcilation(returnFiber, context, lanes),
1468+
lanes,
1469+
);
14451470
}
14461471

14471472
throwOnInvalidObjectType(returnFiber, newChild);

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

+30-5
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';
@@ -47,6 +47,7 @@ import {StrictLegacyMode} from './ReactTypeOfMode';
4747
import {getIsHydrating} from './ReactFiberHydrationContext.old';
4848
import {pushTreeFork} from './ReactFiberTreeContext.old';
4949
import {createThenableState, trackUsedThenable} from './ReactFiberThenable.old';
50+
import {readContextDuringReconcilation} from './ReactFiberNewContext.old';
5051

5152
// This tracks the thenables that are unwrapped during reconcilation.
5253
let thenableState: ThenableState | null = null;
@@ -594,7 +595,12 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
594595
newChild.$$typeof === REACT_CONTEXT_TYPE ||
595596
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
596597
) {
597-
// TODO: Implement Context as child type.
598+
const context: ReactContext<mixed> = (newChild: any);
599+
return createChild(
600+
returnFiber,
601+
readContextDuringReconcilation(returnFiber, context, lanes),
602+
lanes,
603+
);
598604
}
599605

600606
throwOnInvalidObjectType(returnFiber, newChild);
@@ -679,7 +685,13 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
679685
newChild.$$typeof === REACT_CONTEXT_TYPE ||
680686
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
681687
) {
682-
// TODO: Implement Context as child type.
688+
const context: ReactContext<mixed> = (newChild: any);
689+
return updateSlot(
690+
returnFiber,
691+
oldFiber,
692+
readContextDuringReconcilation(returnFiber, context, lanes),
693+
lanes,
694+
);
683695
}
684696

685697
throwOnInvalidObjectType(returnFiber, newChild);
@@ -762,7 +774,14 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
762774
newChild.$$typeof === REACT_CONTEXT_TYPE ||
763775
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
764776
) {
765-
// TODO: Implement Context as child type.
777+
const context: ReactContext<mixed> = (newChild: any);
778+
return updateFromMap(
779+
existingChildren,
780+
returnFiber,
781+
newIdx,
782+
readContextDuringReconcilation(returnFiber, context, lanes),
783+
lanes,
784+
);
766785
}
767786

768787
throwOnInvalidObjectType(returnFiber, newChild);
@@ -1441,7 +1460,13 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
14411460
newChild.$$typeof === REACT_CONTEXT_TYPE ||
14421461
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
14431462
) {
1444-
// TODO: Implement Context as child type.
1463+
const context: ReactContext<mixed> = (newChild: any);
1464+
return reconcileChildFibersImpl(
1465+
returnFiber,
1466+
currentFirstChild,
1467+
readContextDuringReconcilation(returnFiber, context, lanes),
1468+
lanes,
1469+
);
14451470
}
14461471

14471472
throwOnInvalidObjectType(returnFiber, newChild);

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

+20-3
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

+20-3
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)