cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

A tour of the React repository

Find your way around React's source at v19.3.1: the packages, how react-dom plugs into the reconciler, and how npm's builds map back to the code.

By the end, you can
  • 01
    Find where a feature livesName the folder and file in facebook/react that holds useState, createRoot, the work loop, the DOM code, and the scheduler.
  • 02
    Explain how renderers plug inDescribe how react hands hooks to the renderer through a dispatcher, and how the reconciler calls a host config to change the DOM.
  • 03
    Read the source and the buildsMap the files npm installs back to the source, read __DEV__ blocks and feature flags, and follow React's functions in a stack trace.

Three imports, one repository

This stage reads React's own code. Before you open a single function, you need a map: which folder holds what, and how the pieces you install connect. Start with a small app you already know how to read. Click its button.

An app that prints React's versionPop out in the code editor (opens in a new tab)JSX
import { useState, version } from "react";import { version as domVersion } from "react-dom"; export default function App() {  const [count, setCount] = useState(0);   return (    <>      <p>react: {version}</p>      <p>react-dom: {domVersion}</p>      <button onClick={() => setCount(count + 1)}>Add tea ({count})</button>    </>  );}
ResultLoading…

Line 1 imports useState and version from the react package. Line 2 imports version from a second package, react-dom, and renames it domVersion. Lines 9 and 10 print both: 19.3.1 and 19.3.1. The two packages always ship together, with the same number.

There are two more imports you don't see. Before the code runs, a compiler turns the JSX on lines 8 to 12 into calls to jsx(), imported from react/jsx-runtime (the Writing markup with JSX lesson shows this). And every live example has an entry file, main.jsx, which you can see with Pop out:

main.jsx: the entry file every live example getsJSX
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App />
  </StrictMode>
);

Line 2 imports createRoot from react-dom/client, and line 5 hands it the page's <div id="root">. So one small app reads code from react, react/jsx-runtime, and react-dom/client, and react-dom quietly loads a fourth package, scheduler. All of them are built from one GitHub repository, facebook/react. Every link in this lesson opens it at the tag v19.3.1, the release these examples run. A tag never changes, so line numbers stay right; the main branch changes every day.

A map of the facebook/react repository. A crate named packages holds two columns of cards. On npm: react, react-dom, react-reconciler, scheduler. Copied into other packages: shared, react-dom-bindings, react-server, react-client. A second crate, compiler, holds the React Compiler.on npmcopied into othersreactreact-domreact-reconcilerschedulersharedreact-dom-bindingsreact-serverreact-clientpackages/its own workspaceReact Compilercompiler/TypeScript, runsin your build tool
The repository's two main folders. packages/ holds React itself: the four folders on the left are published to npm, and the four on the right are never installed on their own; their code is copied into the packages that use them. compiler/ holds the React Compiler.
Real-life analogyA play on tour

A touring play has one script and one director. In each city, a local crew builds the sets with whatever that theatre has. The writer never needs to know which stage the play is on, and the director gives every crew the same kind of instructions.

In real life: The script the writer hands in
In React: Your components, written with the react package
In real life: The director, who works out what changes between scenes
In React: The reconciler, which works out what changed between renders
In real life: Each theatre's stage crew, who move that building's own sets and lights
In React: A host config, which creates and changes one host's nodes
In real life: The same play opening in a new theatre
In React: The same components shown by another renderer, like React Native
The main idea

React is one repository that builds many npm packages. You write components with react, which describes the UI. A renderer such as react-dom shows it. Inside every renderer is the same reconciler, which works out what changed, and a host config, the renderer's own functions that create and change real nodes.

The packages folder: one repository, many packages

A repository that holds many packages is called a monorepo. React's packages sit side by side in packages/, one folder each, and the build turns some of them into the npm packages you install. Others are never installed on their own: the build copies their code into the packages that use them. In the code, a file imports another package by its name, such as import ... from 'shared/ReactSymbols', even when that package is never published.

How to read the table: each row is one folder, the name npm knows it by, and what it holds.

The folders you will read most
FolderOn npm asWhat it holds
packages/reactreactEverything you import from react: the hooks, createElement, the JSX runtime, memo, lazy. It describes UI and never draws it.
packages/react-domreact-domThe web renderer's entry points: createRoot, hydrateRoot, the server renderers, and APIs such as preload.
packages/react-dom-bindingsNot publishedThe DOM code: the host config that creates and changes DOM nodes, and the event system. It is built into react-dom.
packages/react-reconcilerreact-reconciler, for custom renderersFibers, the work loop, hooks, and the commit phase. Every renderer copies it into its own build.
packages/schedulerschedulerRuns tasks by priority and lets the browser respond between them. react-dom loads it at run time.
packages/sharedNot publishedSmall pieces many packages use: the element symbols, the feature flags, helpers such as assign.
packages/react-serverNot publishedServer rendering: Fizz writes HTML, and Flight sends Server Components. It is built into react-dom/server and the Server Components packages.
packages/react-clientNot publishedThe Flight client, which reads a Server Components stream in the browser.
compiler/babel-plugin-react-compiler and othersThe React Compiler, a workspace of its own, written in TypeScript.

Three things stand out. First, react is small: it holds the functions you call, but none of the code that renders. Second, the reconciler and the DOM code are separate folders, but you get both inside react-dom's files. Third, the compiler/ folder is a workspace of its own, written in TypeScript, with its own packages; it runs while your app is built, not in the browser. Other folders hold more renderers (react-native-renderer, react-art, react-test-renderer), the DevTools, and the hooks lint rules in eslint-plugin-react-hooks.

Now try the map. Pick something you use every day, and the playground shows the folder, the files and lines at v19.3.1, and the file under node_modules where the same code ends up.

Playground: find where it lives
Where it livespackages/react
You write
const [count, setCount] = useState(0);
useState
packages/react/src/ReactHooks.js, line 66Two lines: it asks for the current dispatcher and calls its useState.
mountState
packages/react-reconciler/src/ReactFiberHooks.js, line 1948The real work on the first render: it makes the hook and its update queue for this component.
In node_modules
react/cjs/react.development.jsSearch for return resolveDispatcher().useState(initialState);
Try it yourself
useState

useState is exported by react, but react only passes the call on. The code that keeps your state is in the reconciler, which react-dom carries inside its own file.

Every link opens the file on GitHub at the tag v19.3.1, the release the live examples run, at the line named. The last row is the file npm installs, under node_modules, and a line you can search for in it.

Try useState first. You import it from react, but the first file is only a doorway: the state is kept by mountState in the reconciler. Then try A <li> appearing on the page: in the built file, createInstance has no function of its own any more, because the build copied it into the function that calls it. Searching for a distinctive line, like createElement(type), finds it anyway.

react describes, the renderer does the work

If react has no rendering code, how does useState work? Here is the whole function, from ReactHooks.js. (The source uses Flow, a type checker like TypeScript: <S> and the text after each colon are types, and you can read past them.)

useState in ReactHooks.js, lines 66 to 71JavaScript
export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

In plain words, line by line:

  1. Lines 1 to 3 declare useState: it takes a starting value and returns a pair.
  2. Line 4 asks resolveDispatcher() for the current dispatcher: an object holding the real versions of every hook.
  3. Line 5 calls that object's useState and returns whatever it returns. That's all.

resolveDispatcher is in the same file, at lines 24 to 42:

resolveDispatcher in ReactHooks.js, lines 24 to 42JavaScript
function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error(
        'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
          ' one of the following reasons:\n' +
          '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
          '2. You might be breaking the Rules of Hooks\n' +
          '3. You might have more than one copy of React in the same app\n' +
          'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
      );
    }
  }
  // Will result in a null access error if accessed outside render phase. We
  // intentionally don't throw our own error because this is in a hot path.
  // Also helps ensure this is inlined.
  return dispatcher as any as Dispatcher;
}
  1. Line 2 reads ReactSharedInternals.H. ReactSharedInternals is an object that react exports so renderers can reach it, and H is the slot for the hooks dispatcher.
  2. Lines 3 to 14 only exist in development builds (__DEV__, explained below): if the slot is empty, React prints the Invalid hook call message.
  3. Lines 15 to 18 return the slot as it is, even when it's null. The comment says why: this runs on every hook call, so React doesn't spend time on a check of its own.

So who fills the slot? The reconciler, just before it calls your component. This is the production branch of renderWithHooks in ReactFiberHooks.js, lines 562 to 567:

renderWithHooks fills the slot, ReactFiberHooks.js lines 562 to 567JavaScript
  } else {
    ReactSharedInternals.H =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }

Lines 2 to 5 put one of two dispatchers in H: HooksDispatcherOnMount the first time a component renders (current, React's record of the component from the last render, is null, or it has no saved hooks), and HooksDispatcherOnUpdate after that. That's how the same useState(0) line creates state on the first render and reads it on later ones. When the component returns, renderWithHooks calls finishRenderingHooks, which puts ContextOnlyDispatcher back, at line 660.

Left, a function card useState(0) from the react package. Middle, a box named H holding a card that says dispatcher. Right, a function card renderWithHooks from react-dom. An arrow from renderWithHooks puts the dispatcher into H. An arrow from useState reads H.useState(0)reactreads the slotdispatcherHa shared slotrenderWithHooksreact-domfills it while rendering
react's useState only reads one slot, H. While react-dom renders your component, renderWithHooks puts a dispatcher there, the object with the real hook functions. Outside a render the slot holds null, or a dispatcher whose hooks throw.

You can watch the slot yourself. react exports ReactSharedInternals under a name that warns you off, __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, at line 110 of ReactClient.js. Its contents change between releases, so never use it in an app; here it's only a window to look through. Click the button.

Look at the hooks slot before, during, and after a renderPop out in the code editor (opens in a new tab)JSX
import { useState, __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE as internals } from "react"; const beforeAnyRender = internals.H; export default function App() {  const [afterRender, setAfterRender] = useState("not clicked yet");  const duringRender = internals.H;   function check() {    try {      internals.H.useState(0);      setAfterRender("useState worked");    } catch (error) {      setAfterRender(error.message.split(".")[0]);    }  }   return (    <>      <p>Before any render: {String(beforeAnyRender)}</p>      <p>During this render: an object with {Object.keys(duringRender).length} hooks</p>      <button onClick={check}>Call H.useState from a click</button>      <p>From a click: {afterRender}</p>    </>  );}
ResultLoading…

Line 3 runs when the file loads, before anything has rendered: the result shows Before any render: null. Line 7 runs inside App, during a render: H is the dispatcher, an object with 24 hook functions, from readContext and use to useEffectEvent. The button calls H.useState from a click, after the render is over, and line 14 shows the first sentence of the error: Invalid hook call.

Why an error, and not null again? The work loop explains it, in ReactFiberWorkLoop.js:

pushDispatcher in ReactFiberWorkLoop.js, lines 2489 to 2500JavaScript
function pushDispatcher(container: any) {
  const prevDispatcher = ReactSharedInternals.H;
  ReactSharedInternals.H = ContextOnlyDispatcher;
  if (prevDispatcher === null) {
    // The React isomorphic package does not include a default dispatcher.
    // Instead the first renderer will lazily attach one, in order to give
    // nicer error messages.
    return ContextOnlyDispatcher;
  } else {
    return prevDispatcher;
  }
}

Before each render, line 3 sets H to ContextOnlyDispatcher, a dispatcher whose hooks all throw. The function returns what was there before (line 10), and when the render ends, the work loop puts that back. On the very first render there was nothing, so lines 4 to 8 return ContextOnlyDispatcher instead. The comment says it: react ships with no dispatcher, and the first renderer will lazily attach one, in order to give nicer error messages. From then on, a hook called outside a render throws a clear error instead of reading null.

The reconciler and host configs

The reconciler, in packages/react-reconciler, is the engine: it keeps a fiber, a small record, for each component and element (the fiber lesson later in this stage opens one), calls your components, compares the new elements with the old, and decides what to create, change, or remove. It never touches a DOM node. Every time it needs the host, it calls a function it imports from one file:

The reconciler's commit code imports the host's functions, ReactFiberCommitHostEffects.js (shortened)JavaScript
import {
  supportsMutation,
  supportsResources,
  supportsSingletons,
  commitMount,
  commitUpdate,
  resetTextContent,
  commitTextUpdate,
  appendChild,
  appendChildToContainer,
  insertBefore,
  // … 17 more
} from './ReactFiberConfig';

This is the top of ReactFiberCommitHostEffects.js, which applies changes during the commit. Each name is a job only the host knows how to do: commitUpdate changes an element's props, commitTextUpdate changes text, appendChild and insertBefore place a node. Together, these functions are the host config. But open ReactFiberConfig.js itself, and it holds one line of code:

ReactFiberConfig.js, line 20JavaScript
throw new Error('This module must be shimmed by a specific renderer.');

The file is a placeholder. When React builds react-dom, the build tool swaps it for a fork, a replacement file named after the renderer. scripts/rollup/forks.js does the swap, and forks/ holds one file per host: art, custom, dom, fabric (React Native), markup, noop, and test. The DOM one is two lines:

forks/ReactFiberConfig.dom.js, lines 10 and 11JavaScript
export * from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
export * from 'react-client/src/ReactClientConsoleConfigBrowser';

Line 1 passes on everything from ReactFiberConfigDOM.js, the DOM host config. Line 2 adds how React prints to the browser's console. Inside the DOM host config, the functions are plain DOM code. Here is the one that changes text, at lines 1018 to 1024:

commitTextUpdate in ReactFiberConfigDOM.js, lines 1018 to 1024JavaScript
export function commitTextUpdate(
  textInstance: TextInstance,
  oldText: string,
  newText: string,
): void {
  textInstance.nodeValue = newText;
}

Lines 2 to 4 are the text node, the old text, and the new text. Line 6 is the whole job: it sets the text node's nodeValue, the same thing you'd do by hand (the Node properties lesson covers nodeValue). A test renderer does something else entirely. Its createInstance, in ReactFiberConfigTestHost.js, makes no DOM node at all:

createInstance in the test renderer's host config, lines 158 to 174JavaScript
export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: Object,
  internalInstanceHandle: Object,
): Instance {
  return {
    type,
    props,
    isHidden: false,
    children: [],
    internalInstanceHandle,
    rootContainerInstance,
    tag: 'INSTANCE',
  };
}

Lines 8 to 16 return a plain object with the element's type, its props, and an empty children array. Same reconciler, same call, a different host. React Native's createInstance asks the phone for a native view with createNode instead.

A function card, react-reconciler, at the top. Three arrows lead down to three windows: a web page holding an li element, made by react-dom with createElement; a phone app holding a native view, made by react-native with createNode; and tests holding a plain object with type li, made by react-test-renderer.react-reconcilerweb page<li>react-domcreateElementphone appnative viewreact-nativecreateNodetests{ type: "li" }react-test-renderera plain object
One reconciler decides what to create, change, and remove. Each renderer gives it a host config, a set of functions such as createInstance and appendChild, that do the work for its host: DOM elements, native views, or plain objects in a test.

You can watch the DOM host config work. The second file below, spy.js, replaces document.createElement (lines 35 to 43 of its tab) so that every <ul> and <li> React creates gets watched: its appendChild, insertBefore, and removeChild (lines 9 to 20), and writes to its textContent (lines 21 to 32), are recorded before they run. App.jsx shows the record after each commit, with a layout effect (lines 8 to 10). Click the three buttons in order.

Spy on the DOM calls react-dom makesPop out in the code editor (opens in a new tab)JSX
import { useLayoutEffect, useState } from "react";import { calls } from "./spy.js"; export default function App() {  const [items, setItems] = useState(["Tea", "Cake"]);  const [log, setLog] = useState([]);   useLayoutEffect(() => {    if (calls.length > 0) setLog(calls.splice(0));  });   return (    <>      <button onClick={() => setItems([...items, "Lemonade"])}>Add lemonade</button>      <button onClick={() => setItems(["Mint", ...items])}>Add mint first</button>      <button onClick={() => setItems(items.slice(1))}>Remove first</button>      <ul>        {items.map((name) => (          <li key={name}>{name}</li>        ))}      </ul>      <pre>{log.join("\n")}</pre>    </>  );}
ResultLoading…

The record answers questions you could only guess at before:

  1. First render. React creates the two <li> elements and sets their text, then creates the <ul>, then appends the items to it. It builds from the leaves up, while rendering, before anything is on the page. (The last call, putting the <ul> into #root, isn't recorded: the spy only watches lists.)
  2. Add lemonade. One new <li>, then ul.appendChild: the host config's appendChild. Tea and Cake are untouched.
  3. Add mint first. A new item at the front needs insertBefore, placed before Tea.
  4. Remove first. One call, removeChild, for Mint. The other items stay where they are, because their keys didn't change.

From source files to the files npm installs

Nobody installs the source. When you install react, npm gives you files that React's build made from it. The entry point you import is tiny. This is packages/react/npm/jsx-runtime.js, which npm installs as node_modules/react/jsx-runtime.js:

react/jsx-runtime.js: the file your bundler opensJavaScript
'use strict';

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react-jsx-runtime.production.js');
} else {
  module.exports = require('./cjs/react-jsx-runtime.development.js');
}

Line 3 reads process.env.NODE_ENV, a value your bundler sets: "production" for the build you ship, "development" while you work. Line 4 loads the production file, line 6 the development file. Both sit in the package's cjs folder (CommonJS, the older module format with require; see CommonJS and ES modules). The pattern repeats for react, react-dom/client, and the rest.

The two files come from the same source. The build is a Rollup script, scripts/rollup/build.js, and this part decides what __DEV__ means:

scripts/rollup/build.js, lines 434 to 445JavaScript
      // Turn __DEV__ and process.env checks into constants.
      replace({
        preventAssignment: true,
        values: {
          __DEV__: isProduction ? 'false' : 'true',
          __PROFILE__: isProfiling || !isProduction ? 'true' : 'false',
          'process.env.NODE_ENV': isProduction
            ? "'production'"
            : "'development'",
          __EXPERIMENTAL__,
        },
      }),
  1. Line 5 replaces every __DEV__ in the source with the text false for a production file and true for a development file. So __DEV__ isn't a variable your app can change; it's gone before you install React.
  2. Line 6 does the same for __PROFILE__, which turns on timing code for the profiler.
  3. Lines 7 to 9 write process.env.NODE_ENV as a fixed string into each file.
  4. Line 10 fills in __EXPERIMENTAL__: true for React's experimental channel, false for a stable release like 19.3.1.

After the replacement, a block like if (false) { … } can never run, and the build removes it. That's how one source file gives a development file full of warnings and a production file with none. The same goes for feature flags, the switches in packages/shared/ReactFeatureFlags.js that turn unfinished features on or off. Here is one, at line 110:

A feature flag in ReactFeatureFlags.js, line 110JavaScript
export const enableOptimisticKey = __EXPERIMENTAL__;

enableOptimisticKey is on only in experimental builds. When you read the source and meet if (enableOptimisticKey && …), look the flag up in this file: in your stable 19.3.1 build, that branch doesn't exist. (Builds for Meta's own apps and React Native use other copies of this file, in packages/shared/forks.)

A source card, ReactJSXElement.js, with two arrows to two built files: jsx-runtime.development.js, where __DEV__ is true, and jsx-runtime.production.js, where __DEV__ is false. Below them, the npm entry jsx-runtime.js points at both and picks one by NODE_ENV.ReactJSXElement.jsthe source__DEV__ true__DEV__ falsejsx-runtime.development.jsjsx-runtime.production.jsjsx-runtime.jsthe npm entry picks oneby NODE_ENV
The build turns one source file into two. Every __DEV__ becomes true in the development file and false in the production file, and code that can never run is removed. react/jsx-runtime.js, the file your bundler opens, picks one by process.env.NODE_ENV.

See it on a real function. jsxProd builds an element in production. Switch between the source, the production file, and what jsx is in the development file:

Playground: one function, three files
jsxProd in ReactJSXElement.js, lines 291 to 348 (the source at v19.3.1)JavaScript
export function jsxProd(type, config, maybeKey) {
  let key = null;

  // Currently, key can be spread in as a prop. This causes a potential
  // issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
  // or <div key="Hi" {...props} /> ). We want to deprecate key spread,
  // but as an intermediary step, we will use jsxDEV for everything except
  // <div {...props} key="Hi" />, because we aren't currently able to tell if
  // key is explicitly declared to be undefined or not.
  if (maybeKey !== undefined) {
    if (enableOptimisticKey && maybeKey === REACT_OPTIMISTIC_KEY) {
      key = REACT_OPTIMISTIC_KEY;
    } else {
      if (__DEV__) {
        checkKeyStringCoercion(maybeKey);
      }
      key = '' + maybeKey;
    }
  }

  if (hasValidKey(config)) {
    if (enableOptimisticKey && maybeKey === REACT_OPTIMISTIC_KEY) {
      key = REACT_OPTIMISTIC_KEY;
    } else {
      if (__DEV__) {
        checkKeyStringCoercion(config.key);
      }
      key = '' + config.key;
    }
  }

  let props;
  if (!('key' in config)) {
    // If key was not spread in, we can reuse the original props object. This
    // only works for `jsx`, not `createElement`, because `jsx` is a compiler
    // target and the compiler always passes a new object. For `createElement`,
    // we can't assume a new object is passed every time because it can be
    // called manually.
    //
    // Spreading key is a warning in dev. In a future release, we will not
    // remove a spread key from the props object. (But we'll still warn.) We'll
    // always pass the object straight through.
    props = config;
  } else {
    // We need to remove reserved props (key, prop, ref). Create a fresh props
    // object and copy over all the non-reserved props. We don't use `delete`
    // because in V8 it will deopt the object to dictionary mode.
    props = {};
    for (const propName in config) {
      // Skip over reserved prop names
      if (propName !== 'key') {
        props[propName] = config[propName];
      }
    }
  }

  return ReactElement(type, key, props, getOwner(), undefined, undefined);
}
Try it yourself
Source: 58 lines

The function as React's team writes it: 58 lines, with comments, two checks of the feature flag enableOptimisticKey, and two __DEV__ blocks. It ends by calling ReactElement, which builds the object.

Each version is copied exactly: the source from the tag v19.3.1 on GitHub, the two built files from the react@19.3.1 package on npm.

In Source, find the two enableOptimisticKey checks and the two __DEV__ blocks. In Production build, all four are gone, and the 58 lines became 18. Comments are gone too, and ReactElement, a separate function in the source, has been copied into its only caller, so the object is built right there. Development build shows that in development, jsx is a different function, one that records extra information for warnings. You can open these files yourself, in your own project, under node_modules/react/cjs/.

How to read the table: each row asks one question of both builds.

The development and production builds
QuestionDevelopment buildProduction build
Which file does npm's entry load?cjs/react-jsx-runtime.development.js, when NODE_ENV isn't production.cjs/react-jsx-runtime.production.js, when NODE_ENV is production.
What happens to if (__DEV__) blocks?They stay, and __DEV__ is true, so warnings and checks run.They are removed, because __DEV__ is false and the code can never run.
Are elements frozen?Yes. Object.freeze runs on each element and its props.No. The freezing code was in a __DEV__ block.
How do error messages look?Full sentences, like the Invalid hook call text.A number and a link to react.dev/errors, to keep the file small.
Can you read it?Yes: names and structure match the source, without comments or types.Mostly: names stay, but small functions are copied into their callers.

Reading the source with a debugger

Reading files top to bottom is slow. A faster way is to stop React in the middle of its work and look at who called whom. That list of calls is the call stack (see The call stack), and every Error carries a copy of it as text in its stack property (see The stack trace API). This example reads that text inside App and lists the names, from App down:

Who called my component?Pop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; function callers() {  Error.stackTraceLimit = 50;  const lines = new Error().stack.split("\n");  const names = lines.map((line) => /at (?:Object\.)?([\w$]+) /.exec(line)?.[1] ?? /^(?:Object\.)?([\w$]+)@/.exec(line)?.[1]);  return names.slice(names.indexOf("App")).filter(Boolean);} export default function App() {  const [count, setCount] = useState(0);  const names = callers();   return (    <>      <button onClick={() => setCount(count + 1)}>Render again ({count})</button>      <ol>        {names.map((name, index) => (          <li key={index}>{name}</li>        ))}      </ol>    </>  );}
ResultLoading…

Line 4 asks for up to 50 lines of stack, since browsers keep only a few by default. Line 5 makes an Error without throwing it and splits its stack into lines. Line 6 picks each function's name out of a line; Chrome writes at renderWithHooks (…) and Firefox and Safari write renderWithHooks@…, so it tries both. Line 7 keeps the names from App down. Each name is a real function in React's development build, and each one is in the source:

The first render's stack, from your component down
Name in the stackFile at v19.3.1What it does
AppYour file.Your component, called by React.
react_stack_bottom_frameReactFiberCallUserSpace.jsA one-line wrapper that calls your component in development, so React knows where your code starts.
renderWithHooks (and renderWithHooksAgain)ReactFiberHooks.jsPuts the hooks dispatcher in place and calls your component.
updateFunctionComponent, beginWorkReactFiberBeginWork.jsDecide what kind of fiber this is and start its work.
performUnitOfWork, workLoopSync, renderRootSync, performWorkOnRootReactFiberWorkLoop.jsWalk the tree one fiber at a time until the render is done.

Read from the bottom up, it's the update you met in Render and commit: performWorkOnRoot starts a render, workLoopSync loops over fibers, performUnitOfWork takes one, beginWork sees a function component, and renderWithHooks calls it. The work loop lesson later in this stage reads each of them.

Why is renderWithHooksAgain there, above renderWithHooks? Live examples run in StrictMode, which calls each component twice in development to catch impure code. The list you see comes from the second call. Here is where that happens, in ReactFiberHooks.js:

renderWithHooks calls your component, then again in StrictMode (ReactFiberHooks.js, shortened)JavaScript
  let children = __DEV__
    ? callComponentInDEV(Component, props, secondArg)
    : Component(props, secondArg);
  // …
  if (shouldDoubleRenderDEV) {
    // In development, components are invoked twice to help detect side effects.
    setIsStrictModeForDevtools(true);
    try {
      children = renderWithHooksAgain(
        workInProgress,
        Component,
        props,
        secondArg,
      );
    } finally {
      setIsStrictModeForDevtools(false);
    }
  }

Lines 1 to 3 call your component: through callComponentInDEV in development, which is the react_stack_bottom_frame wrapper, and directly in production. Lines 5 to 18 run only when StrictMode is on, and line 9 calls it again. Look at the bottom of the list, too. The first render ends in performWorkOnRootViaSchedulerTask and performWorkUntilDeadline: the scheduler ran it as a task. Now press Render again. The top of the list stays the same, but the bottom changes: a click's update runs through performSyncWorkOnRoot and processRootScheduleInMicrotask, in ReactFiberRootScheduler.js, because React handles a click's render in a microtask right after your handler.

To step through it yourself, in your own project:

  1. Put debugger; on a line inside your component, or click a line number in the browser's Sources panel to set a breakpoint (Debugging in the browser covers both). Reload the page.
  2. When it stops, look at the Call Stack panel. Chrome hides library code by default, as "ignore-listed" frames; right-click the list and choose to show them, and React's functions appear, the same names as above.
  3. Click a name, like beginWork, to open React's development file at that line. With Vite it is node_modules/.vite/deps/react-dom_client.js, built from react-dom/cjs/react-dom-client.development.js. Step from there, or search the file for a function name.
  4. When you want the comments and the full code, open the same function at the tag on GitHub; the names match. In the source, skip if (__DEV__) blocks on a first read, and look up any flag in ReactFeatureFlags.js.

Tracing an Invalid hook call to its source

The map pays off when React prints something you don't understand. "Invalid hook call" is a common one, and you've now seen both places that print it. Here is a cart whose Save button calls a hook from its click handler, a bug the Rules of Hooks lesson warns about. Click Save cart.

A hook called from a click handler (a bug on purpose)Pop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; export default function App() {  const [message, setMessage] = useState("Click to save the cart.");   function saveCart() {    try {      const [saved] = useState(false);      setMessage("Saved: " + saved);    } catch (error) {      setMessage(error.message.split("\n")[0]);    }  }   return (    <>      <button onClick={saveCart}>Save cart</button>      <p>{message}</p>    </>  );}
ResultLoading…

Line 8 calls useState while the handler runs. The render finished long ago, so the slot H holds ContextOnlyDispatcher, whose useState is throwInvalidHookError. It throws, line 11 catches the error and shows its first line. Without the try, the error would stop the handler.

The other version of the message comes from resolveDispatcher, when H is null. In a working app, that happens when your component and react-dom use two different copies of react, say because a library bundled its own. react-dom fills H in the copy it imports; your component reads H from the other copy, still null. React prints the message, then dispatcher.useState reads a property of null and throws. How to read the table: each row is one way the message appears.

Two places that say Invalid hook call
What you seeWhere it comes fromUsual cause
A console.error, then Cannot read properties of null (reading 'useState')resolveDispatcher in ReactHooks.js: the slot H is null.No render is running in this copy of React: a hook called before anything rendered, or a second copy of react that react-dom never fills.
A thrown Error: Invalid hook call…throwInvalidHookError in ReactFiberHooks.js: H holds ContextOnlyDispatcher.A hook called outside a component's body after React has rendered: in an event handler, a timer, or an effect's callback.

The second row is why the message lists "more than one copy of React" as a reason. To check a real project, run npm ls react: it prints every copy npm installed, and there should be one. Now sort a few functions into the folders that hold them:

Which folder holds it?
  • useState
  • createElement
  • isValidElement
  • beginWork
  • workLoopSync
  • createInstance
  • commitTextUpdate
  • unstable_scheduleCallback
  • shouldYieldToHost
Try it yourself
0 of 9 correct

Sort each function into the folder of facebook/react where it's defined at v19.3.1.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "The react package renders my components." No. It has no rendering code. useState is two lines that pass your call to a dispatcher, and the code that renders ships inside react-dom.
  • "My app installs react-reconciler." No. Each renderer copies the reconciler into its own files. The react-reconciler package on npm is for people who write their own renderer.
  • "__DEV__ is a variable React checks while my app runs." No. The build replaced it with true or false and removed the blocks that can never run. Your bundler's NODE_ENV only picks which file loads.
  • "Feature flags are settings I can turn on." No. They are fixed when React is built. A stable release has every __EXPERIMENTAL__ flag off.
  • "The production build can't be read." No. React 19's production files keep their function names. They lose the warnings and comments, and small functions are copied into their callers.
  • "The main branch on GitHub is the code I run." No. main changes every day. Read the tag for your version, here v19.3.1.

Practice exercises

The live exercises show their result as you edit. Use the map, the playgrounds, and the links above; every answer is in a file you've seen.

Exercise 1 · Warm-upFind createRoot's folder

Without the playground: which folder of facebook/react holds the createRoot your main.jsx imports?

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict the key's type

    Line 3 calls jsx() yourself, with the key 3, a number. What type is item.key? Answer, then press Edit & run to check.

    import { jsx } from "react/jsx-runtime";
    
    const item = jsx("li", { children: "Green tea" }, 3);
    
    export default function App() {
      return <p>The key is {JSON.stringify(item.key)}, a {typeof item.key}.</p>;
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeIs the slot empty again?

      The app has rendered once. In a click handler, is internals.H null? Predict true or false, then press Edit & run and click.

      import { useState, __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE as internals } from "react";
      
      export default function App() {
        const [answer, setAnswer] = useState("?");
      
        return (
          <>
            <button onClick={() => setAnswer(String(internals.H === null))}>Is H null now?</button>
            <p>In a click handler, H is null: {answer}</p>
          </>
        );
      }

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeSpy on a removal from the end

        Add a Remove last button to the spy example. Click Add lemonade, then Remove last. Which call does the record show?

        import { useLayoutEffect, useState } from "react";import { calls } from "./spy.js"; export default function App() {  const [items, setItems] = useState(["Tea", "Cake"]);  const [log, setLog] = useState([]);   useLayoutEffect(() => {    if (calls.length > 0) setLog(calls.splice(0));  });   return (    <>      <button onClick={() => setItems([...items, "Lemonade"])}>Add lemonade</button>      <button onClick={() => setItems(["Mint", ...items])}>Add mint first</button>      <button onClick={() => setItems(items.slice(1))}>Remove first</button>      <ul>        {items.map((name) => (          <li key={name}>{name}</li>        ))}      </ul>      <pre>{log.join("\n")}</pre>    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFind the microtask in the stack

          Press Render again in this example. One function near the bottom of the list says, in its name, how React ran this render. Which one?

          import { useState } from "react"; function callers() {  Error.stackTraceLimit = 50;  const lines = new Error().stack.split("\n");  const names = lines.map((line) => /at (?:Object\.)?([\w$]+) /.exec(line)?.[1] ?? /^(?:Object\.)?([\w$]+)@/.exec(line)?.[1]);  return names.slice(names.indexOf("App")).filter(Boolean);} export default function App() {  const [count, setCount] = useState(0);  const names = callers();   return (    <>      <button onClick={() => setCount(count + 1)}>Render again ({count})</button>      <ol>        {names.map((name, index) => (          <li key={index}>{name}</li>        ))}      </ol>    </>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeIs the experimental branch there?

            Symbol.for("react.optimistic_key") is the value the enableOptimisticKey branch looks for. In React 19.3.1, what happens when you pass it as a key? Predict, then press Edit & run.

            import { jsx } from "react/jsx-runtime";
            
            let result;
            try {
              jsx("li", { children: "Green tea" }, Symbol.for("react.optimistic_key"));
              result = "No error: the experimental branch ran.";
            } catch (error) {
              result = error.name + ": " + error.message;
            }
            
            export default function App() {
              return <p>{result}</p>;
            }

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask which package or file is doing the work, and when: while React is built, or while your app runs.

              The React source tour quiz · 8 questionsScore: first tries count
              1. Question 1 of 8Which npm package's file holds the code that calls document.createElement for your <li>?

                Choose an answer to see the explanation.

              2. Question 2 of 8This is the whole app, in a fresh preview. What does it show?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE as internals } from "react";
                
                const atStart = internals.H;
                
                export default function App() {
                  return <p>{String(atStart)}</p>;
                }

                Choose an answer to see the explanation.

              3. Question 3 of 8These lines are from React's production build. What is key after jsxProd("li", { key: 7 })?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function jsxProd(type, config, maybeKey) {
                  var key = null;
                  void 0 !== maybeKey && (key = "" + maybeKey);
                  void 0 !== config.key && (key = "" + config.key);
                  // …
                }

                Choose an answer to see the explanation.

              4. Question 4 of 8Why does react-reconciler/src/ReactFiberConfig.js contain only throw new Error('This module must be shimmed by a specific renderer.')?

                Choose an answer to see the explanation.

              5. Question 5 of 8What happens to an if (__DEV__) { … } block in React's production file?

                Choose an answer to see the explanation.

              6. Question 6 of 8An app logs "Invalid hook call" with console.error, then throws Cannot read properties of null (reading 'useState'). Which cause fits best?

                Choose an answer to see the explanation.

              7. Question 7 of 8In the "Who called my component?" list, why does renderWithHooksAgain sit between react_stack_bottom_frame and renderWithHooks?

                Choose an answer to see the explanation.

              8. Question 8 of 8Where in the repository is the React Compiler?

                Choose an answer to see the explanation.

              Key takeaways

              • facebook/react is a monorepo. packages/ holds React, compiler/ holds the React Compiler. Read it at your version's tag, here v19.3.1.
              • react describes UI and passes hook calls on through the slot H. The renderer fills that slot while it renders your component.
              • The reconciler works out what changed and calls a host config to do it. react-dom carries the reconciler and the DOM host config, from react-dom-bindings, in its own files.
              • The build replaces __DEV__, __EXPERIMENTAL__, and the feature flags with fixed values, and removes code that can never run. npm's entry file picks the development or production file by NODE_ENV.
              • A stack trace, or a breakpoint, shows React's own function names. Look them up at the tag to read the code with its comments.

              Remember the one-liner.
              react describes, the reconciler decides, the host config does, and the build picks what ships.

              Coming next: React elements and the JSX runtime. It opens ReactJSXElement.js and looks at the objects every jsx() call returns, field by field.

              CompleteFrontend Clear concepts. Working examples.