343 lines
10 KiB
JavaScript
343 lines
10 KiB
JavaScript
/**
|
|
* 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,
|
|
};
|