compiler/packages/react-compiler-runtime/src/index.ts TYPESCRIPT 418 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import * as React from 'react';910const {useRef, useEffect, isValidElement} = React;11const ReactSecretInternals =12  //@ts-ignore13  React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ??14  //@ts-ignore15  React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;1617type MemoCache = Array<number | typeof $empty>;1819const $empty = Symbol.for('react.memo_cache_sentinel');2021// Re-export React.c if present, otherwise fallback to the userspace polyfill for versions of React22// < 19.23export const c =24  // @ts-expect-error25  typeof React.__COMPILER_RUNTIME?.c === 'function'26    ? // @ts-expect-error27      React.__COMPILER_RUNTIME.c28    : 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 from35          // useMemoCache.36          // @ts-ignore37          $[$empty] = true;38          return $;39        }, []);40      };4142const 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});7374let originalDispatcher: unknown = null;7576// Allow guards are not emitted for useMemoCache77LazyGuardDispatcher['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};8687enum GuardKind {88  PushGuardContext = 0,89  PopGuardContext = 1,90  PushExpectHook = 2,91  PopExpectHook = 3,92}9394function setCurrent(newDispatcher: any) {95  ReactSecretInternals.ReactCurrentDispatcher.current = newDispatcher;96  return ReactSecretInternals.ReactCurrentDispatcher.current;97}9899const guardFrames: Array<unknown> = [];100101/**102 * When `enableEmitHookGuards` is set, this does runtime validation103 * of the no-conditional-hook-calls rule.104 * As React Compiler needs to statically understand which calls to move out of105 * conditional branches (i.e. React Compiler cannot memoize the results of hook106 * 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 memoize110 * the call to AnotherComponent, introducing conditional hook calls in its111 * compiled output.112 * ```js113 * function Invalid(props) {114 *  const myJsx = AnotherComponent(props);115 *  return <div> { myJsx } </div>;116 * }117 *118 * Hooks must be named as hooks.119 * ```js120 * 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 */136export function $dispatcherGuard(kind: GuardKind) {137  const curr = ReactSecretInternals.ReactCurrentDispatcher.current;138  if (kind === GuardKind.PushGuardContext) {139    // Push before checking invariant or errors140    guardFrames.push(curr);141142    if (guardFrames.length === 1) {143      // save if we're the first guard on the stack144      originalDispatcher = curr;145    }146147    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 errors157    const lastFrame = guardFrames.pop();158159    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 dispatcher170    // 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}185186export function $reset($: MemoCache) {187  for (let ii = 0; ii < $.length; ii++) {188    $[ii] = $empty;189  }190}191192export function $makeReadOnly() {193  throw new Error('TODO: implement $makeReadOnly in react-compiler-runtime');194}195196/**197 * Instrumentation to count rerenders in React components198 */199export const renderCounterRegistry: Map<200  string,201  Set<{count: number}>202> = new Map();203export function clearRenderCounterRegistry() {204  for (const counters of renderCounterRegistry.values()) {205    counters.forEach(counter => {206      counter.count = 0;207    });208  }209}210211function 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}219220function 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}227228export function useRenderCounter(name: string): void {229  const val = useRef<{count: number}>(null);230231  if (val.current != null) {232    val.current.count += 1;233  }234  useEffect(() => {235    // Not counting initial render shouldn't be a problem236    if (val.current == null) {237      const counter = {count: 0};238      registerRenderCounter(name, counter);239      // @ts-ignore240      val.current = counter;241    }242    return () => {243      if (val.current !== null) {244        removeRenderCounter(name, val.current);245      }246    };247  });248}249250const seenErrors = new Set();251252export 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 now402      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}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.