main
js 324 lines 9.63 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {Fiber} from './ReactInternalTypes';
11 import type {StackCursor} from './ReactFiberStack';
12
13 import {disableLegacyContext} from 'shared/ReactFeatureFlags';
14 import {ClassComponent, HostRoot} from './ReactWorkTags';
15 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
16
17 import {createCursor, push, pop} from './ReactFiberStack';
18
19 let warnedAboutMissingGetChildContext;
20
21 if (__DEV__) {
22 warnedAboutMissingGetChildContext = {} as {[string]: boolean};
23 }
24
25 export const emptyContextObject: {} = {};
26 if (__DEV__) {
27 Object.freeze(emptyContextObject);
28 }
29
30 // A cursor to the current merged context object on the stack.
31 const contextStackCursor: StackCursor<Object> =
32 createCursor(emptyContextObject);
33 // A cursor to a boolean indicating whether the context has changed.
34 const didPerformWorkStackCursor: StackCursor<boolean> = createCursor(false);
35 // Keep track of the previous context object that was on the stack.
36 // We use this to get access to the parent context after we have already
37 // pushed the next context provider, and now need to merge their contexts.
38 let previousContext: Object = emptyContextObject;
39
40 function getUnmaskedContext(
41 workInProgress: Fiber,
42 Component: Function,
43 didPushOwnContextIfProvider: boolean,
44 ): Object {
45 if (disableLegacyContext) {
46 return emptyContextObject;
47 } else {
48 if (didPushOwnContextIfProvider && isContextProvider(Component)) {
49 // If the fiber is a context provider itself, when we read its context
50 // we may have already pushed its own child context on the stack. A context
51 // provider should not "see" its own child context. Therefore we read the
52 // previous (parent) context instead for a context provider.
53 return previousContext;
54 }
55 return contextStackCursor.current;
56 }
57 }
58
59 function cacheContext(
60 workInProgress: Fiber,
61 unmaskedContext: Object,
62 maskedContext: Object,
63 ): void {
64 if (disableLegacyContext) {
65 return;
66 } else {
67 const instance = workInProgress.stateNode;
68 instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext;
69 instance.__reactInternalMemoizedMaskedChildContext = maskedContext;
70 }
71 }
72
73 function getMaskedContext(
74 workInProgress: Fiber,
75 unmaskedContext: Object,
76 ): Object {
77 if (disableLegacyContext) {
78 return emptyContextObject;
79 } else {
80 const type = workInProgress.type;
81 const contextTypes = type.contextTypes;
82 if (!contextTypes) {
83 return emptyContextObject;
84 }
85
86 // Avoid recreating masked context unless unmasked context has changed.
87 // Failing to do this will result in unnecessary calls to componentWillReceiveProps.
88 // This may trigger infinite loops if componentWillReceiveProps calls setState.
89 const instance = workInProgress.stateNode;
90 if (
91 instance &&
92 instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext
93 ) {
94 return instance.__reactInternalMemoizedMaskedChildContext;
95 }
96
97 const context: {[string]: $FlowFixMe} = {};
98 for (const key in contextTypes) {
99 context[key] = unmaskedContext[key];
100 }
101
102 // Cache unmasked context so we can avoid recreating masked context unless necessary.
103 // Context is created before the class component is instantiated so check for instance.
104 if (instance) {
105 cacheContext(workInProgress, unmaskedContext, context);
106 }
107
108 return context;
109 }
110 }
111
112 function hasContextChanged(): boolean {
113 if (disableLegacyContext) {
114 return false;
115 } else {
116 return didPerformWorkStackCursor.current;
117 }
118 }
119
120 function isContextProvider(type: Function): boolean {
121 if (disableLegacyContext) {
122 return false;
123 } else {
124 const childContextTypes = type.childContextTypes;
125 return childContextTypes !== null && childContextTypes !== undefined;
126 }
127 }
128
129 function popContext(fiber: Fiber): void {
130 if (disableLegacyContext) {
131 return;
132 } else {
133 pop(didPerformWorkStackCursor, fiber);
134 pop(contextStackCursor, fiber);
135 }
136 }
137
138 function popTopLevelContextObject(fiber: Fiber): void {
139 if (disableLegacyContext) {
140 return;
141 } else {
142 pop(didPerformWorkStackCursor, fiber);
143 pop(contextStackCursor, fiber);
144 }
145 }
146
147 function pushTopLevelContextObject(
148 fiber: Fiber,
149 context: Object,
150 didChange: boolean,
151 ): void {
152 if (disableLegacyContext) {
153 return;
154 } else {
155 if (contextStackCursor.current !== emptyContextObject) {
156 throw new Error(
157 'Unexpected context found on stack. ' +
158 'This error is likely caused by a bug in React. Please file an issue.',
159 );
160 }
161
162 push(contextStackCursor, context, fiber);
163 push(didPerformWorkStackCursor, didChange, fiber);
164 }
165 }
166
167 function processChildContext(
168 fiber: Fiber,
169 type: any,
170 parentContext: Object,
171 ): Object {
172 if (disableLegacyContext) {
173 return parentContext;
174 } else {
175 const instance = fiber.stateNode;
176 const childContextTypes = type.childContextTypes;
177
178 // TODO (bvaughn) Replace this behavior with an invariant() in the future.
179 // It has only been added in Fiber to match the (unintentional) behavior in Stack.
180 if (typeof instance.getChildContext !== 'function') {
181 if (__DEV__) {
182 const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
183
184 if (!warnedAboutMissingGetChildContext[componentName]) {
185 warnedAboutMissingGetChildContext[componentName] = true;
186 console.error(
187 '%s.childContextTypes is specified but there is no getChildContext() method ' +
188 'on the instance. You can either define getChildContext() on %s or remove ' +
189 'childContextTypes from it.',
190 componentName,
191 componentName,
192 );
193 }
194 }
195 return parentContext;
196 }
197
198 const childContext = instance.getChildContext();
199 for (const contextKey in childContext) {
200 if (!(contextKey in childContextTypes)) {
201 throw new Error(
202 `${
203 getComponentNameFromFiber(fiber) || 'Unknown'
204 }.getChildContext(): key "${contextKey}" is not defined in childContextTypes.`,
205 );
206 }
207 }
208
209 return {...parentContext, ...childContext};
210 }
211 }
212
213 function pushContextProvider(workInProgress: Fiber): boolean {
214 if (disableLegacyContext) {
215 return false;
216 } else {
217 const instance = workInProgress.stateNode;
218 // We push the context as early as possible to ensure stack integrity.
219 // If the instance does not exist yet, we will push null at first,
220 // and replace it on the stack later when invalidating the context.
221 const memoizedMergedChildContext =
222 (instance && instance.__reactInternalMemoizedMergedChildContext) ||
223 emptyContextObject;
224
225 // Remember the parent context so we can merge with it later.
226 // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates.
227 previousContext = contextStackCursor.current;
228 push(contextStackCursor, memoizedMergedChildContext, workInProgress);
229 push(
230 didPerformWorkStackCursor,
231 didPerformWorkStackCursor.current,
232 workInProgress,
233 );
234
235 return true;
236 }
237 }
238
239 function invalidateContextProvider(
240 workInProgress: Fiber,
241 type: any,
242 didChange: boolean,
243 ): void {
244 if (disableLegacyContext) {
245 return;
246 } else {
247 const instance = workInProgress.stateNode;
248
249 if (!instance) {
250 throw new Error(
251 'Expected to have an instance by this point. ' +
252 'This error is likely caused by a bug in React. Please file an issue.',
253 );
254 }
255
256 if (didChange) {
257 // Merge parent and own context.
258 // Skip this if we're not updating due to sCU.
259 // This avoids unnecessarily recomputing memoized values.
260 const mergedContext = processChildContext(
261 workInProgress,
262 type,
263 previousContext,
264 );
265 instance.__reactInternalMemoizedMergedChildContext = mergedContext;
266
267 // Replace the old (or empty) context with the new one.
268 // It is important to unwind the context in the reverse order.
269 pop(didPerformWorkStackCursor, workInProgress);
270 pop(contextStackCursor, workInProgress);
271 // Now push the new context and mark that it has changed.
272 push(contextStackCursor, mergedContext, workInProgress);
273 push(didPerformWorkStackCursor, didChange, workInProgress);
274 } else {
275 pop(didPerformWorkStackCursor, workInProgress);
276 push(didPerformWorkStackCursor, didChange, workInProgress);
277 }
278 }
279 }
280
281 function findCurrentUnmaskedContext(fiber: Fiber): Object {
282 if (disableLegacyContext) {
283 return emptyContextObject;
284 } else {
285 // Currently this is only used with renderSubtreeIntoContainer; not sure if it
286 // makes sense elsewhere
287 let node: Fiber = fiber;
288 do {
289 switch (node.tag) {
290 case HostRoot:
291 return node.stateNode.context;
292 case ClassComponent: {
293 const Component = node.type;
294 if (isContextProvider(Component)) {
295 return node.stateNode.__reactInternalMemoizedMergedChildContext;
296 }
297 break;
298 }
299 }
300 // $FlowFixMe[incompatible-type] we bail out when we get a null
301 node = node.return;
302 } while (node !== null);
303
304 throw new Error(
305 'Found unexpected detached subtree parent. ' +
306 'This error is likely caused by a bug in React. Please file an issue.',
307 );
308 }
309 }
310
311 export {
312 getUnmaskedContext,
313 cacheContext,
314 getMaskedContext,
315 hasContextChanged,
316 popContext,
317 popTopLevelContextObject,
318 pushTopLevelContextObject,
319 processChildContext,
320 isContextProvider,
321 pushContextProvider,
322 invalidateContextProvider,
323 findCurrentUnmaskedContext,
324 };