| 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 | } |