main
ts 417 lines 12.1 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
8 import * as React from 'react';
9
10 const {useRef, useEffect, isValidElement} = React;
11 const ReactSecretInternals =
12 //@ts-ignore
13 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ??
14 //@ts-ignore
15 React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
16
17 type MemoCache = Array<number | typeof $empty>;
18
19 const $empty = Symbol.for('react.memo_cache_sentinel');
20
21 // Re-export React.c if present, otherwise fallback to the userspace polyfill for versions of React
22 // < 19.
23 export const c =
24 // @ts-expect-error
25 typeof React.__COMPILER_RUNTIME?.c === 'function'
26 ? // @ts-expect-error
27 React.__COMPILER_RUNTIME.c
28 : function c(size: number) {
29 return React.useMemo<Array<unknown>>(() => {
30 const $ = new Array(size);
31 for (let ii = 0; ii < size; ii++) {
32 $[ii] = $empty;
33 }
34 // This symbol is added to tell the react devtools that this array is from
35 // useMemoCache.
36 // @ts-ignore
37 $[$empty] = true;
38 return $;
39 }, []);
40 };
41
42 const LazyGuardDispatcher: {[key: string]: (...args: Array<any>) => any} = {};
43 [
44 'readContext',
45 'useCallback',
46 'useContext',
47 'useEffect',
48 'useImperativeHandle',
49 'useInsertionEffect',
50 'useLayoutEffect',
51 'useMemo',
52 'useReducer',
53 'useRef',
54 'useState',
55 'useDebugValue',
56 'useDeferredValue',
57 'useTransition',
58 'useMutableSource',
59 'useSyncExternalStore',
60 'useId',
61 'unstable_isNewReconciler',
62 'getCacheSignal',
63 'getCacheForType',
64 'useCacheRefresh',
65 ].forEach(name => {
66 LazyGuardDispatcher[name] = () => {
67 throw new Error(
68 `[React] Unexpected React hook call (${name}) from a React compiled function. ` +
69 "Check that all hooks are called directly and named according to convention ('use[A-Z]') ",
70 );
71 };
72 });
73
74 let originalDispatcher: unknown = null;
75
76 // Allow guards are not emitted for useMemoCache
77 LazyGuardDispatcher['useMemoCache'] = (count: number) => {
78 if (originalDispatcher == null) {
79 throw new Error(
80 'React Compiler internal invariant violation: unexpected null dispatcher',
81 );
82 } else {
83 return (originalDispatcher as any).useMemoCache(count);
84 }
85 };
86
87 enum GuardKind {
88 PushGuardContext = 0,
89 PopGuardContext = 1,
90 PushExpectHook = 2,
91 PopExpectHook = 3,
92 }
93
94 function setCurrent(newDispatcher: any) {
95 ReactSecretInternals.ReactCurrentDispatcher.current = newDispatcher;
96 return ReactSecretInternals.ReactCurrentDispatcher.current;
97 }
98
99 const guardFrames: Array<unknown> = [];
100
101 /**
102 * When `enableEmitHookGuards` is set, this does runtime validation
103 * of the no-conditional-hook-calls rule.
104 * As React Compiler needs to statically understand which calls to move out of
105 * conditional branches (i.e. React Compiler cannot memoize the results of hook
106 * calls), its understanding of "the rules of React" are more restrictive.
107 * This validation throws on unsound inputs at runtime.
108 *
109 * Components should only be invoked through React as React Compiler could memoize
110 * the call to AnotherComponent, introducing conditional hook calls in its
111 * compiled output.
112 * ```js
113 * function Invalid(props) {
114 * const myJsx = AnotherComponent(props);
115 * return <div> { myJsx } </div>;
116 * }
117 *
118 * Hooks must be named as hooks.
119 * ```js
120 * const renamedHook = useState;
121 * function Invalid() {
122 * const [state, setState] = renamedHook(0);
123 * }
124 * ```
125 *
126 * Hooks must be directly called.
127 * ```
128 * function call(fn) {
129 * return fn();
130 * }
131 * function Invalid() {
132 * const result = call(useMyHook);
133 * }
134 * ```
135 */
136 export function $dispatcherGuard(kind: GuardKind) {
137 const curr = ReactSecretInternals.ReactCurrentDispatcher.current;
138 if (kind === GuardKind.PushGuardContext) {
139 // Push before checking invariant or errors
140 guardFrames.push(curr);
141
142 if (guardFrames.length === 1) {
143 // save if we're the first guard on the stack
144 originalDispatcher = curr;
145 }
146
147 if (curr === LazyGuardDispatcher) {
148 throw new Error(
149 `[React] Unexpected call to custom hook or component from a React compiled function. ` +
150 "Check that (1) all hooks are called directly and named according to convention ('use[A-Z]') " +
151 'and (2) components are returned as JSX instead of being directly invoked.',
152 );
153 }
154 setCurrent(LazyGuardDispatcher);
155 } else if (kind === GuardKind.PopGuardContext) {
156 // Pop before checking invariant or errors
157 const lastFrame = guardFrames.pop();
158
159 if (lastFrame == null) {
160 throw new Error(
161 'React Compiler internal error: unexpected null in guard stack',
162 );
163 }
164 if (guardFrames.length === 0) {
165 originalDispatcher = null;
166 }
167 setCurrent(lastFrame);
168 } else if (kind === GuardKind.PushExpectHook) {
169 // ExpectHooks could be nested, so we save the current dispatcher
170 // for the matching PopExpectHook to restore.
171 guardFrames.push(curr);
172 setCurrent(originalDispatcher);
173 } else if (kind === GuardKind.PopExpectHook) {
174 const lastFrame = guardFrames.pop();
175 if (lastFrame == null) {
176 throw new Error(
177 'React Compiler internal error: unexpected null in guard stack',
178 );
179 }
180 setCurrent(lastFrame);
181 } else {
182 throw new Error('React Compiler internal error: unreachable block' + kind);
183 }
184 }
185
186 export function $reset($: MemoCache) {
187 for (let ii = 0; ii < $.length; ii++) {
188 $[ii] = $empty;
189 }
190 }
191
192 export function $makeReadOnly() {
193 throw new Error('TODO: implement $makeReadOnly in react-compiler-runtime');
194 }
195
196 /**
197 * Instrumentation to count rerenders in React components
198 */
199 export const renderCounterRegistry: Map<
200 string,
201 Set<{count: number}>
202 > = new Map();
203 export function clearRenderCounterRegistry() {
204 for (const counters of renderCounterRegistry.values()) {
205 counters.forEach(counter => {
206 counter.count = 0;
207 });
208 }
209 }
210
211 function registerRenderCounter(name: string, val: {count: number}) {
212 let counters = renderCounterRegistry.get(name);
213 if (counters == null) {
214 counters = new Set();
215 renderCounterRegistry.set(name, counters);
216 }
217 counters.add(val);
218 }
219
220 function removeRenderCounter(name: string, val: {count: number}): void {
221 const counters = renderCounterRegistry.get(name);
222 if (counters == null) {
223 return;
224 }
225 counters.delete(val);
226 }
227
228 export function useRenderCounter(name: string): void {
229 const val = useRef<{count: number}>(null);
230
231 if (val.current != null) {
232 val.current.count += 1;
233 }
234 useEffect(() => {
235 // Not counting initial render shouldn't be a problem
236 if (val.current == null) {
237 const counter = {count: 0};
238 registerRenderCounter(name, counter);
239 // @ts-ignore
240 val.current = counter;
241 }
242 return () => {
243 if (val.current !== null) {
244 removeRenderCounter(name, val.current);
245 }
246 };
247 });
248 }
249
250 const seenErrors = new Set();
251
252 export function $structuralCheck(
253 oldValue: any,
254 newValue: any,
255 variableName: string,
256 fnName: string,
257 kind: string,
258 loc: string,
259 ): void {
260 function error(l: string, r: string, path: string, depth: number) {
261 const str = `${fnName}:${loc} [${kind}] ${variableName}${path} changed from ${l} to ${r} at depth ${depth}`;
262 if (seenErrors.has(str)) {
263 return;
264 }
265 seenErrors.add(str);
266 console.error(str);
267 }
268 const depthLimit = 2;
269 function recur(oldValue: any, newValue: any, path: string, depth: number) {
270 if (depth > depthLimit) {
271 return;
272 } else if (oldValue === newValue) {
273 return;
274 } else if (typeof oldValue !== typeof newValue) {
275 error(`type ${typeof oldValue}`, `type ${typeof newValue}`, path, depth);
276 } else if (typeof oldValue === 'object') {
277 const oldArray = Array.isArray(oldValue);
278 const newArray = Array.isArray(newValue);
279 if (oldValue === null && newValue !== null) {
280 error('null', `type ${typeof newValue}`, path, depth);
281 } else if (newValue === null) {
282 error(`type ${typeof oldValue}`, 'null', path, depth);
283 } else if (oldValue instanceof Map) {
284 if (!(newValue instanceof Map)) {
285 error(`Map instance`, `other value`, path, depth);
286 } else if (oldValue.size !== newValue.size) {
287 error(
288 `Map instance with size ${oldValue.size}`,
289 `Map instance with size ${newValue.size}`,
290 path,
291 depth,
292 );
293 } else {
294 for (const [k, v] of oldValue) {
295 if (!newValue.has(k)) {
296 error(
297 `Map instance with key ${k}`,
298 `Map instance without key ${k}`,
299 path,
300 depth,
301 );
302 } else {
303 recur(v, newValue.get(k), `${path}.get(${k})`, depth + 1);
304 }
305 }
306 }
307 } else if (newValue instanceof Map) {
308 error('other value', `Map instance`, path, depth);
309 } else if (oldValue instanceof Set) {
310 if (!(newValue instanceof Set)) {
311 error(`Set instance`, `other value`, path, depth);
312 } else if (oldValue.size !== newValue.size) {
313 error(
314 `Set instance with size ${oldValue.size}`,
315 `Set instance with size ${newValue.size}`,
316 path,
317 depth,
318 );
319 } else {
320 for (const v of newValue) {
321 if (!oldValue.has(v)) {
322 error(
323 `Set instance without element ${v}`,
324 `Set instance with element ${v}`,
325 path,
326 depth,
327 );
328 }
329 }
330 }
331 } else if (newValue instanceof Set) {
332 error('other value', `Set instance`, path, depth);
333 } else if (oldArray || newArray) {
334 if (oldArray !== newArray) {
335 error(
336 `type ${oldArray ? 'array' : 'object'}`,
337 `type ${newArray ? 'array' : 'object'}`,
338 path,
339 depth,
340 );
341 } else if (oldValue.length !== newValue.length) {
342 error(
343 `array with length ${oldValue.length}`,
344 `array with length ${newValue.length}`,
345 path,
346 depth,
347 );
348 } else {
349 for (let ii = 0; ii < oldValue.length; ii++) {
350 recur(oldValue[ii], newValue[ii], `${path}[${ii}]`, depth + 1);
351 }
352 }
353 } else if (isValidElement(oldValue) || isValidElement(newValue)) {
354 if (isValidElement(oldValue) !== isValidElement(newValue)) {
355 error(
356 `type ${isValidElement(oldValue) ? 'React element' : 'object'}`,
357 `type ${isValidElement(newValue) ? 'React element' : 'object'}`,
358 path,
359 depth,
360 );
361 } else if (oldValue.type !== newValue.type) {
362 error(
363 `React element of type ${oldValue.type}`,
364 `React element of type ${newValue.type}`,
365 path,
366 depth,
367 );
368 } else {
369 recur(
370 oldValue.props,
371 newValue.props,
372 `[props of ${path}]`,
373 depth + 1,
374 );
375 }
376 } else {
377 for (const key in newValue) {
378 if (!(key in oldValue)) {
379 error(
380 `object without key ${key}`,
381 `object with key ${key}`,
382 path,
383 depth,
384 );
385 }
386 }
387 for (const key in oldValue) {
388 if (!(key in newValue)) {
389 error(
390 `object with key ${key}`,
391 `object without key ${key}`,
392 path,
393 depth,
394 );
395 } else {
396 recur(oldValue[key], newValue[key], `${path}.${key}`, depth + 1);
397 }
398 }
399 }
400 } else if (typeof oldValue === 'function') {
401 // Bail on functions for now
402 return;
403 } else if (isNaN(oldValue) || isNaN(newValue)) {
404 if (isNaN(oldValue) !== isNaN(newValue)) {
405 error(
406 `${isNaN(oldValue) ? 'NaN' : 'non-NaN value'}`,
407 `${isNaN(newValue) ? 'NaN' : 'non-NaN value'}`,
408 path,
409 depth,
410 );
411 }
412 } else if (oldValue !== newValue) {
413 error(oldValue, newValue, path, depth);
414 }
415 }
416 recur(oldValue, newValue, '', 0);
417 }