Skip to content

Commit

Permalink
Support Context as renderable node
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Nov 18, 2022
1 parent e593439 commit a665a00
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 17 deletions.
28 changes: 28 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5375,6 +5375,34 @@ describe('ReactDOMFizzServer', () => {

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

it('context as node', async () => {
const Context = React.createContext('Hi');
await act(async () => {
const {pipe} = renderToPipeableStream(Context);
pipe(writable);
});
expect(getVisibleChildren(container)).toEqual('Hi');
});

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

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

describe('useEvent', () => {
Expand Down
35 changes: 30 additions & 5 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 @@ -47,6 +47,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 @@ -594,7 +595,12 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
const context: ReactContext<mixed> = (newChild: any);
return createChild(
returnFiber,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
);
}

throwOnInvalidObjectType(returnFiber, newChild);
Expand Down Expand Up @@ -679,7 +685,13 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
const context: ReactContext<mixed> = (newChild: any);
return updateSlot(
returnFiber,
oldFiber,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
);
}

throwOnInvalidObjectType(returnFiber, newChild);
Expand Down Expand Up @@ -762,7 +774,14 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
const context: ReactContext<mixed> = (newChild: any);
return updateFromMap(
existingChildren,
returnFiber,
newIdx,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
);
}

throwOnInvalidObjectType(returnFiber, newChild);
Expand Down Expand Up @@ -1441,7 +1460,13 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
const context: ReactContext<mixed> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
);
}

throwOnInvalidObjectType(returnFiber, newChild);
Expand Down
35 changes: 30 additions & 5 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 @@ -47,6 +47,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 @@ -594,7 +595,12 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
const context: ReactContext<mixed> = (newChild: any);
return createChild(
returnFiber,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
);
}

throwOnInvalidObjectType(returnFiber, newChild);
Expand Down Expand Up @@ -679,7 +685,13 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
const context: ReactContext<mixed> = (newChild: any);
return updateSlot(
returnFiber,
oldFiber,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
);
}

throwOnInvalidObjectType(returnFiber, newChild);
Expand Down Expand Up @@ -762,7 +774,14 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
const context: ReactContext<mixed> = (newChild: any);
return updateFromMap(
existingChildren,
returnFiber,
newIdx,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
);
}

throwOnInvalidObjectType(returnFiber, newChild);
Expand Down Expand Up @@ -1441,7 +1460,13 @@ function createChildReconciler(shouldTrackSideEffects): ChildReconciler {
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
// TODO: Implement Context as child type.
const context: ReactContext<mixed> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
);
}

throwOnInvalidObjectType(returnFiber, newChild);
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 a665a00

Please sign in to comment.