import type {ReactContext} from 'shared/ReactTypes';
import type {
Fiber,
ContextDependency,
Dependencies,
ContextDependencyWithSelect,
} from './ReactInternalTypes';
import type {StackCursor} from './ReactFiberStack';
import type {Lanes} from './ReactFiberLane';
import type {SharedQueue} from './ReactFiberClassUpdateQueue';
import type {TransitionStatus} from './ReactFiberConfig';
import type {Hook} from './ReactFiberHooks';
import {isPrimaryRenderer, HostTransitionContext} from './ReactFiberConfig';
import {createCursor, push, pop} from './ReactFiberStack';
import {
ContextProvider,
ClassComponent,
DehydratedFragment,
} from './ReactWorkTags';
import {
NoLanes,
isSubsetOfLanes,
includesSomeLane,
mergeLanes,
pickArbitraryLane,
} from './ReactFiberLane';
import {
NoFlags,
DidPropagateContext,
NeedsPropagation,
} from './ReactFiberFlags';
import is from 'shared/objectIs';
import {createUpdate, ForceUpdate} from './ReactFiberClassUpdateQueue';
import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork';
import {
enableLazyContextPropagation,
enableAsyncActions,
enableRenderableContext,
} from 'shared/ReactFeatureFlags';
import {getHostTransitionProvider} from './ReactFiberHostContext';
import isArray from '../../shared/isArray';
import {enableContextProfiling} from '../../shared/ReactFeatureFlags';
const valueCursor: StackCursor<mixed> = createCursor(null);
let rendererCursorDEV: StackCursor<Object | null>;
if (__DEV__) {
rendererCursorDEV = createCursor(null);
}
let renderer2CursorDEV: StackCursor<Object | null>;
if (__DEV__) {
renderer2CursorDEV = createCursor(null);
}
let rendererSigil;
if (__DEV__) {
rendererSigil = {};
}
let currentlyRenderingFiber: Fiber | null = null;
let lastContextDependency:
| ContextDependency<mixed>
| ContextDependencyWithSelect<mixed>
| null = null;
let lastFullyObservedContext: ReactContext<any> | null = null;
let isDisallowedContextReadInDEV: boolean = false;
export function resetContextDependencies(): void {
currentlyRenderingFiber = null;
lastContextDependency = null;
lastFullyObservedContext = null;
if (__DEV__) {
isDisallowedContextReadInDEV = false;
}
}
export function enterDisallowedContextReadInDEV(): void {
if (__DEV__) {
isDisallowedContextReadInDEV = true;
}
}
export function exitDisallowedContextReadInDEV(): void {
if (__DEV__) {
isDisallowedContextReadInDEV = false;
}
}
export function pushProvider<T>(
providerFiber: Fiber,
context: ReactContext<T>,
nextValue: T,
): void {
if (isPrimaryRenderer) {
push(valueCursor, context._currentValue, providerFiber);
context._currentValue = nextValue;
if (__DEV__) {
push(rendererCursorDEV, context._currentRenderer, providerFiber);
if (
context._currentRenderer !== undefined &&
context._currentRenderer !== null &&
context._currentRenderer !== rendererSigil
) {
console.error(
'Detected multiple renderers concurrently rendering the ' +
'same context provider. This is currently unsupported.',
);
}
context._currentRenderer = rendererSigil;
}
} else {
push(valueCursor, context._currentValue2, providerFiber);
context._currentValue2 = nextValue;
if (__DEV__) {
push(renderer2CursorDEV, context._currentRenderer2, providerFiber);
if (
context._currentRenderer2 !== undefined &&
context._currentRenderer2 !== null &&
context._currentRenderer2 !== rendererSigil
) {
console.error(
'Detected multiple renderers concurrently rendering the ' +
'same context provider. This is currently unsupported.',
);
}
context._currentRenderer2 = rendererSigil;
}
}
}
export function popProvider(
context: ReactContext<any>,
providerFiber: Fiber,
): void {
const currentValue = valueCursor.current;
if (isPrimaryRenderer) {
context._currentValue = currentValue;
if (__DEV__) {
const currentRenderer = rendererCursorDEV.current;
pop(rendererCursorDEV, providerFiber);
context._currentRenderer = currentRenderer;
}
} else {
context._currentValue2 = currentValue;
if (__DEV__) {
const currentRenderer2 = renderer2CursorDEV.current;
pop(renderer2CursorDEV, providerFiber);
context._currentRenderer2 = currentRenderer2;
}
}
pop(valueCursor, providerFiber);
}
export function scheduleContextWorkOnParentPath(
parent: Fiber | null,
renderLanes: Lanes,
propagationRoot: Fiber,
) {
let node = parent;
while (node !== null) {
const alternate = node.alternate;
if (!isSubsetOfLanes(node.childLanes, renderLanes)) {
node.childLanes = mergeLanes(node.childLanes, renderLanes);
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
}
} else if (
alternate !== null &&
!isSubsetOfLanes(alternate.childLanes, renderLanes)
) {
alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
} else {
}
if (node === propagationRoot) {
break;
}
node = node.return;
}
if (__DEV__) {
if (node !== propagationRoot) {
console.error(
'Expected to find the propagation root when scheduling context work. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
}
}
export function propagateContextChange<T>(
workInProgress: Fiber,
context: ReactContext<T>,
renderLanes: Lanes,
): void {
if (enableLazyContextPropagation) {
const forcePropagateEntireTree = true;
propagateContextChanges(
workInProgress,
[context],
renderLanes,
forcePropagateEntireTree,
);
} else {
propagateContextChange_eager(workInProgress, context, renderLanes);
}
}
function propagateContextChange_eager<T>(
workInProgress: Fiber,
context: ReactContext<T>,
renderLanes: Lanes,
): void {
if (enableLazyContextPropagation) {
return;
}
let fiber = workInProgress.child;
if (fiber !== null) {
fiber.return = workInProgress;
}
while (fiber !== null) {
let nextFiber;
const list = fiber.dependencies;
if (list !== null) {
nextFiber = fiber.child;
let dependency = list.firstContext;
while (dependency !== null) {
if (dependency.context === context) {
if (fiber.tag === ClassComponent) {
const lane = pickArbitraryLane(renderLanes);
const update = createUpdate(lane);
update.tag = ForceUpdate;
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
} else {
const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
const pending = sharedQueue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
}
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleContextWorkOnParentPath(
fiber.return,
renderLanes,
workInProgress,
);
list.lanes = mergeLanes(list.lanes, renderLanes);
break;
}
dependency = dependency.next;
}
} else if (fiber.tag === ContextProvider) {
nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
} else if (fiber.tag === DehydratedFragment) {
const parentSuspense = fiber.return;
if (parentSuspense === null) {
throw new Error(
'We just came from a parent so we must have had a parent. This is a bug in React.',
);
}
parentSuspense.lanes = mergeLanes(parentSuspense.lanes, renderLanes);
const alternate = parentSuspense.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleContextWorkOnParentPath(
parentSuspense,
renderLanes,
workInProgress,
);
nextFiber = fiber.sibling;
} else {
nextFiber = fiber.child;
}
if (nextFiber !== null) {
nextFiber.return = fiber;
} else {
nextFiber = fiber;
while (nextFiber !== null) {
if (nextFiber === workInProgress) {
nextFiber = null;
break;
}
const sibling = nextFiber.sibling;
if (sibling !== null) {
sibling.return = nextFiber.return;
nextFiber = sibling;
break;
}
nextFiber = nextFiber.return;
}
}
fiber = nextFiber;
}
}
function propagateContextChanges<T>(
workInProgress: Fiber,
contexts: Array<any>,
renderLanes: Lanes,
forcePropagateEntireTree: boolean,
): void {
if (!enableLazyContextPropagation) {
return;
}
let fiber = workInProgress.child;
if (fiber !== null) {
fiber.return = workInProgress;
}
while (fiber !== null) {
let nextFiber;
const list = fiber.dependencies;
if (list !== null) {
nextFiber = fiber.child;
let dep = list.firstContext;
findChangedDep: while (dep !== null) {
const dependency = dep;
const consumer = fiber;
findContext: for (let i = 0; i < contexts.length; i++) {
const context: ReactContext<T> = contexts[i];
if (dependency.context === context) {
if (enableContextProfiling) {
const select = dependency.select;
if (select != null && dependency.lastSelectedValue != null) {
const newValue = isPrimaryRenderer
? dependency.context._currentValue
: dependency.context._currentValue2;
if (
!checkIfSelectedContextValuesChanged(
dependency.lastSelectedValue,
select(newValue),
)
) {
continue findContext;
}
}
}
consumer.lanes = mergeLanes(consumer.lanes, renderLanes);
const alternate = consumer.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleContextWorkOnParentPath(
consumer.return,
renderLanes,
workInProgress,
);
if (!forcePropagateEntireTree) {
nextFiber = null;
}
break findChangedDep;
}
}
dep = dependency.next;
}
} else if (fiber.tag === DehydratedFragment) {
const parentSuspense = fiber.return;
if (parentSuspense === null) {
throw new Error(
'We just came from a parent so we must have had a parent. This is a bug in React.',
);
}
parentSuspense.lanes = mergeLanes(parentSuspense.lanes, renderLanes);
const alternate = parentSuspense.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleContextWorkOnParentPath(
parentSuspense,
renderLanes,
workInProgress,
);
nextFiber = null;
} else {
nextFiber = fiber.child;
}
if (nextFiber !== null) {
nextFiber.return = fiber;
} else {
nextFiber = fiber;
while (nextFiber !== null) {
if (nextFiber === workInProgress) {
nextFiber = null;
break;
}
const sibling = nextFiber.sibling;
if (sibling !== null) {
sibling.return = nextFiber.return;
nextFiber = sibling;
break;
}
nextFiber = nextFiber.return;
}
}
fiber = nextFiber;
}
}
export function lazilyPropagateParentContextChanges(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const forcePropagateEntireTree = false;
propagateParentContextChanges(
current,
workInProgress,
renderLanes,
forcePropagateEntireTree,
);
}
export function propagateParentContextChangesToDeferredTree(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const forcePropagateEntireTree = true;
propagateParentContextChanges(
current,
workInProgress,
renderLanes,
forcePropagateEntireTree,
);
}
function propagateParentContextChanges(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
forcePropagateEntireTree: boolean,
) {
if (!enableLazyContextPropagation) {
return;
}
let contexts = null;
let parent: null | Fiber = workInProgress;
let isInsidePropagationBailout = false;
while (parent !== null) {
if (!isInsidePropagationBailout) {
if ((parent.flags & NeedsPropagation) !== NoFlags) {
isInsidePropagationBailout = true;
} else if ((parent.flags & DidPropagateContext) !== NoFlags) {
break;
}
}
if (parent.tag === ContextProvider) {
const currentParent = parent.alternate;
if (currentParent === null) {
throw new Error('Should have a current fiber. This is a bug in React.');
}
const oldProps = currentParent.memoizedProps;
if (oldProps !== null) {
let context: ReactContext<any>;
if (enableRenderableContext) {
context = parent.type;
} else {
context = parent.type._context;
}
const newProps = parent.pendingProps;
const newValue = newProps.value;
const oldValue = oldProps.value;
if (!is(newValue, oldValue)) {
if (contexts !== null) {
contexts.push(context);
} else {
contexts = [context];
}
}
}
} else if (enableAsyncActions && parent === getHostTransitionProvider()) {
const currentParent = parent.alternate;
if (currentParent === null) {
throw new Error('Should have a current fiber. This is a bug in React.');
}
const oldStateHook: Hook = currentParent.memoizedState;
const oldState: TransitionStatus = oldStateHook.memoizedState;
const newStateHook: Hook = parent.memoizedState;
const newState: TransitionStatus = newStateHook.memoizedState;
if (oldState !== newState) {
if (contexts !== null) {
contexts.push(HostTransitionContext);
} else {
contexts = [HostTransitionContext];
}
}
}
parent = parent.return;
}
if (contexts !== null) {
propagateContextChanges(
workInProgress,
contexts,
renderLanes,
forcePropagateEntireTree,
);
}
workInProgress.flags |= DidPropagateContext;
}
function checkIfSelectedContextValuesChanged(
oldComparedValue: Array<mixed>,
newComparedValue: Array<mixed>,
): boolean {
if (isArray(oldComparedValue) && isArray(newComparedValue)) {
if (oldComparedValue.length !== newComparedValue.length) {
return true;
}
for (let i = 0; i < oldComparedValue.length; i++) {
if (!is(newComparedValue[i], oldComparedValue[i])) {
return true;
}
}
} else {
throw new Error('Compared context values must be arrays');
}
return false;
}
export function checkIfContextChanged(
currentDependencies: Dependencies,
): boolean {
if (!enableLazyContextPropagation) {
return false;
}
let dependency = currentDependencies.firstContext;
while (dependency !== null) {
const context = dependency.context;
const newValue = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
const oldValue = dependency.memoizedValue;
if (
enableContextProfiling &&
dependency.select != null &&
dependency.lastSelectedValue != null
) {
if (
checkIfSelectedContextValuesChanged(
dependency.lastSelectedValue,
dependency.select(newValue),
)
) {
return true;
}
} else {
if (!is(newValue, oldValue)) {
return true;
}
}
dependency = dependency.next;
}
return false;
}
export function prepareToReadContext(
workInProgress: Fiber,
renderLanes: Lanes,
): void {
currentlyRenderingFiber = workInProgress;
lastContextDependency = null;
lastFullyObservedContext = null;
const dependencies = workInProgress.dependencies;
if (dependencies !== null) {
if (enableLazyContextPropagation) {
dependencies.firstContext = null;
} else {
const firstContext = dependencies.firstContext;
if (firstContext !== null) {
if (includesSomeLane(dependencies.lanes, renderLanes)) {
markWorkInProgressReceivedUpdate();
}
dependencies.firstContext = null;
}
}
}
}
export function readContextAndCompare<C>(
context: ReactContext<C>,
select: C => Array<mixed>,
): C {
if (!(enableLazyContextPropagation && enableContextProfiling)) {
throw new Error('Not implemented.');
}
return readContextForConsumer_withSelect(
currentlyRenderingFiber,
context,
select,
);
}
export function readContext<T>(context: ReactContext<T>): T {
if (__DEV__) {
if (isDisallowedContextReadInDEV) {
console.error(
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
}
return readContextForConsumer(currentlyRenderingFiber, context);
}
export function readContextDuringReconciliation<T>(
consumer: Fiber,
context: ReactContext<T>,
renderLanes: Lanes,
): T {
if (currentlyRenderingFiber === null) {
prepareToReadContext(consumer, renderLanes);
}
return readContextForConsumer(consumer, context);
}
function readContextForConsumer_withSelect<C>(
consumer: Fiber | null,
context: ReactContext<C>,
select: C => Array<mixed>,
): C {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
} else {
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
select: ((select: any): (context: mixed) => Array<mixed>),
lastSelectedValue: select(value),
};
if (lastContextDependency === 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. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
lastContextDependency = contextItem;
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
} else {
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value;
}
function readContextForConsumer<C>(
consumer: Fiber | null,
context: ReactContext<C>,
): C {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
} else {
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
if (lastContextDependency === 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. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
lastContextDependency = contextItem;
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
} else {
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value;
}