/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Fiber} from './ReactInternalTypes';
import type {StackCursor} from './ReactFiberStack';
import {isFiberMounted} from './ReactFiberTreeReflection';
import {disableLegacyContext} from 'shared/ReactFeatureFlags';
import {ClassComponent, HostRoot} from './ReactWorkTags';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import checkPropTypes from 'shared/checkPropTypes';
import {createCursor, push, pop} from './ReactFiberStack';
let warnedAboutMissingGetChildContext;
if (__DEV__) {
warnedAboutMissingGetChildContext = ({}: {[string]: boolean});
}
export const emptyContextObject: {} = {};
if (__DEV__) {
Object.freeze(emptyContextObject);
}
// A cursor to the current merged context object on the stack.
const contextStackCursor: StackCursor<Object> =
createCursor(emptyContextObject);
// A cursor to a boolean indicating whether the context has changed.
const didPerformWorkStackCursor: StackCursor<boolean> = createCursor(false);
// Keep track of the previous context object that was on the stack.
// We use this to get access to the parent context after we have already
// pushed the next context provider, and now need to merge their contexts.
let previousContext: Object = emptyContextObject;
function getUnmaskedContext(
workInProgress: Fiber,
Component: Function,
didPushOwnContextIfProvider: boolean,
): Object {
if (disableLegacyContext) {
return emptyContextObject;
} else {
if (didPushOwnContextIfProvider && isContextProvider(Component)) {
// If the fiber is a context provider itself, when we read its context
// we may have already pushed its own child context on the stack. A context
// provider should not "see" its own child context. Therefore we read the
// previous (parent) context instead for a context provider.
return previousContext;
}
return contextStackCursor.current;
}
}
function cacheContext(
workInProgress: Fiber,
unmaskedContext: Object,
maskedContext: Object,
): void {
if (disableLegacyContext) {
return;
} else {
const instance = workInProgress.stateNode;
instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext;
instance.__reactInternalMemoizedMaskedChildContext = maskedContext;
}
}
function getMaskedContext(
workInProgress: Fiber,
unmaskedContext: Object,
): Object {
if (disableLegacyContext) {
return emptyContextObject;
} else {
const type = workInProgress.type;
const contextTypes = type.contextTypes;
if (!contextTypes) {
return emptyContextObject;
}
// Avoid recreating masked context unless unmasked context has changed.
// Failing to do this will result in unnecessary calls to componentWillReceiveProps.
// This may trigger infinite loops if componentWillReceiveProps calls setState.
const instance = workInProgress.stateNode;
if (
instance &&
instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext
) {
return instance.__reactInternalMemoizedMaskedChildContext;
}
const context: {[string]: $FlowFixMe} = {};
for (const key in contextTypes) {
context[key] = unmaskedContext[key];
}
if (__DEV__) {
const name = getComponentNameFromFiber(workInProgress) || 'Unknown';
checkPropTypes(contextTypes, context, 'context', name);
}
// Cache unmasked context so we can avoid recreating masked context unless necessary.
// Context is created before the class component is instantiated so check for instance.
if (instance) {
cacheContext(workInProgress, unmaskedContext, context);
}
return context;
}
}
function hasContextChanged(): boolean {
if (disableLegacyContext) {
return false;
} else {
return didPerformWorkStackCursor.current;
}
}
function isContextProvider(type: Function): boolean {
if (disableLegacyContext) {
return false;
} else {
const childContextTypes = type.childContextTypes;
return childContextTypes !== null && childContextTypes !== undefined;
}
}
function popContext(fiber: Fiber): void {
if (disableLegacyContext) {
return;
} else {
pop(didPerformWorkStackCursor, fiber);
pop(contextStackCursor, fiber);
}
}
function popTopLevelContextObject(fiber: Fiber): void {
if (disableLegacyContext) {
return;
} else {
pop(didPerformWorkStackCursor, fiber);
pop(contextStackCursor, fiber);
}
}
function pushTopLevelContextObject(
fiber: Fiber,
context: Object,
didChange: boolean,
): void {
if (disableLegacyContext) {
return;
} else {
if (contextStackCursor.current !== emptyContextObject) {
throw new Error(
'Unexpected context found on stack. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
push(contextStackCursor, context, fiber);
push(didPerformWorkStackCursor, didChange, fiber);
}
}
function processChildContext(
fiber: Fiber,
type: any,
parentContext: Object,
): Object {
if (disableLegacyContext) {
return parentContext;
} else {
const instance = fiber.stateNode;
const childContextTypes = type.childContextTypes;
// TODO (bvaughn) Replace this behavior with an invariant() in the future.
// It has only been added in Fiber to match the (unintentional) behavior in Stack.
if (typeof instance.getChildContext !== 'function') {
if (__DEV__) {
const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
if (!warnedAboutMissingGetChildContext[componentName]) {
warnedAboutMissingGetChildContext[componentName] = true;
console.error(
'%s.childContextTypes is specified but there is no getChildContext() method ' +
'on the instance. You can either define getChildContext() on %s or remove ' +
'childContextTypes from it.',
componentName,
componentName,
);
}
}
return parentContext;
}
const childContext = instance.getChildContext();
for (const contextKey in childContext) {
if (!(contextKey in childContextTypes)) {
throw new Error(
`${
getComponentNameFromFiber(fiber) || 'Unknown'
}.getChildContext(): key "${contextKey}" is not defined in childContextTypes.`,
);
}
}
if (__DEV__) {
const name = getComponentNameFromFiber(fiber) || 'Unknown';
checkPropTypes(childContextTypes, childContext, 'child context', name);
}
return {...parentContext, ...childContext};
}
}
function pushContextProvider(workInProgress: Fiber): boolean {
if (disableLegacyContext) {
return false;
} else {
const instance = workInProgress.stateNode;
// We push the context as early as possible to ensure stack integrity.
// If the instance does not exist yet, we will push null at first,
// and replace it on the stack later when invalidating the context.
const memoizedMergedChildContext =
(instance && instance.__reactInternalMemoizedMergedChildContext) ||
emptyContextObject;
// Remember the parent context so we can merge with it later.
// Inherit the parent's did-perform-work value to avoid inadvertently blocking updates.
previousContext = contextStackCursor.current;
push(contextStackCursor, memoizedMergedChildContext, workInProgress);
push(
didPerformWorkStackCursor,
didPerformWorkStackCursor.current,
workInProgress,
);
return true;
}
}
function invalidateContextProvider(
workInProgress: Fiber,
type: any,
didChange: boolean,
): void {
if (disableLegacyContext) {
return;
} else {
const instance = workInProgress.stateNode;
if (!instance) {
throw new Error(
'Expected to have an instance by this point. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
if (didChange) {
// Merge parent and own context.
// Skip this if we're not updating due to sCU.
// This avoids unnecessarily recomputing memoized values.
const mergedContext = processChildContext(
workInProgress,
type,
previousContext,
);
instance.__reactInternalMemoizedMergedChildContext = mergedContext;
// Replace the old (or empty) context with the new one.
// It is important to unwind the context in the reverse order.
pop(didPerformWorkStackCursor, workInProgress);
pop(contextStackCursor, workInProgress);
// Now push the new context and mark that it has changed.
push(contextStackCursor, mergedContext, workInProgress);
push(didPerformWorkStackCursor, didChange, workInProgress);
} else {
pop(didPerformWorkStackCursor, workInProgress);
push(didPerformWorkStackCursor, didChange, workInProgress);
}
}
}
function findCurrentUnmaskedContext(fiber: Fiber): Object {
if (disableLegacyContext) {
return emptyContextObject;
} else {
// Currently this is only used with renderSubtreeIntoContainer; not sure if it
// makes sense elsewhere
if (!isFiberMounted(fiber) || fiber.tag !== ClassComponent) {
throw new Error(
'Expected subtree parent to be a mounted class component. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
let node: Fiber = fiber;
do {
switch (node.tag) {
case HostRoot:
return node.stateNode.context;
case ClassComponent: {
const Component = node.type;
if (isContextProvider(Component)) {
return node.stateNode.__reactInternalMemoizedMergedChildContext;
}
break;
}
}
// $FlowFixMe[incompatible-type] we bail out when we get a null
node = node.return;
} while (node !== null);
throw new Error(
'Found unexpected detached subtree parent. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
}
export {
getUnmaskedContext,
cacheContext,
getMaskedContext,
hasContextChanged,
popContext,
popTopLevelContextObject,
pushTopLevelContextObject,
processChildContext,
isContextProvider,
pushContextProvider,
invalidateContextProvider,
findCurrentUnmaskedContext,
};