Open-Source Wikis

/

React

/

How to contribute

/

Patterns and conventions

facebook/react

Patterns and conventions

The repo's style is enforced by Prettier (prettier-3 for almost everything, prettier-2 for some compiler legacy code), ESLint (.eslintrc.js plus the custom rules in scripts/eslint-rules/), and Flow (for the runtime). Beyond that, there are several conventions that aren't auto-enforced but show up in nearly every file.

File layout

  • One module per file. Top-of-file copyright header (BSD-MIT) is auto-checked.
  • Type imports use import type { X } from '...' (Flow / TypeScript).
  • Internal cross-package imports go through packages/shared/. A custom rule scripts/eslint-rules/no-cross-fork-imports.js enforces that no package directly imports from another renderer or from a fork-specific file.
  • Everything in packages/<pkg>/src/forks/ is fork-resolved at build time; do not import from another package's forks/ directly.

Module entry points

A package's package.json typically declares many entry points via the exports field, with a react-server condition. The shape is:

{
  "name": "react",
  "exports": {
    ".": {
      "react-server": "./react.react-server.js",
      "default": "./index.js"
    },
    "./jsx-runtime": {
      "react-server": "./jsx-runtime.react-server.js",
      "default": "./jsx-runtime.js"
    }
  }
}

The react-server condition is what lets react and react-dom expose a different surface when they're running on the server side of an RSC pipeline (no client hooks, etc.). Your bundler (or the npm exports resolver) picks the right file.

Errors and invariants

Runtime errors are written as plain throw new Error('...'). The build-time tool scripts/error-codes/extract-errors.js finds these literal strings, registers them in scripts/error-codes/codes.json, and rewrites the production bundle to throw an integer code instead:

// In source:
throw new Error(
  'Hooks can only be called inside the body of a function component.'
);

// In production bundle:
throw Error(formatProdErrorMessage(321));

Conventions:

  • Always include enough context that the message is understandable on its own; a user on the npm bundle will see the message via react.dev/errors/321.
  • Prefer throw new Error(...) over invariants. (The old invariant(cond, msg) helper has been removed in favor of plain if (!cond) throw new Error(msg);.)
  • For developer-only diagnostics, use console.error(...) inside an if (__DEV__) block. The print-warnings script (scripts/print-warnings/) collects them all.

__DEV__, __PROFILE__, __EXPERIMENTAL__

These are globals that the Rollup replace plugin substitutes per-build:

Global Meaning
__DEV__ True in development, false in production. Gates all warnings and assertion-only code.
__PROFILE__ True in profiling builds. Gates code that emits Performance marks for the Profiler tab.
__EXPERIMENTAL__ True in the experimental channel. Gates large unfinished features when adding a flag is too granular.
__VARIANT__ True for variant builds; rarely used outside very specific A/B feature flags.

Always wrap dev-only code in if (__DEV__) { ... } so that the production bundle dead-code-eliminates it. The same goes for __PROFILE__.

Feature flags

Every new feature lands behind a flag in packages/shared/ReactFeatureFlags.js. The pattern:

// Per-channel forks override the default:
//   packages/shared/forks/ReactFeatureFlags.www.js
//   packages/shared/forks/ReactFeatureFlags.www-modern.js
//   packages/shared/forks/ReactFeatureFlags.test-renderer.js
//   packages/shared/forks/ReactFeatureFlags.native-fb.js
//   packages/shared/forks/ReactFeatureFlags.native-oss.js

// In source:
import { enableNewFeature } from 'shared/ReactFeatureFlags';

// In packages/shared/ReactFeatureFlags.js
export const enableNewFeature = false;

if (enableNewFeature) {
  /* ... */
}

Tests gate on the flag with the @gate pragma. See testing.

The yarn flags script in the repo root runs scripts/flags/flags.js, which prints a table of every flag and its value per channel — useful when you want to know what a fresh react@experimental actually enables.

Style

Prettier's defaults plus:

  • 80-column line length where possible (Prettier's default).
  • Single quotes for strings.
  • Trailing commas everywhere.
  • Arrow functions for callbacks; named function declarations for top-level functions.

Compiler code uses TypeScript and follows the same Prettier config. Compiler tests use the snap golden-file format described in compiler/CLAUDE.md.

Naming

  • File names: ReactFiberX.js, ReactDOMY.js — the package prefix matches the package the file belongs to.
  • Internal-only files use the Internal suffix or live under a forks/ directory.
  • Test files: <Name>-test.js (or -test.internal.js for tests that mock private flags).

Avoiding dependencies

The runtime has very few production dependencies — by design. New dependencies are heavily scrutinized. The dev-dependency list (in the root package.json) is allowed to grow, but anything that ends up in a published bundle has to clear a high bar.

Cross-package imports — the shared package

packages/shared/ is the dumping ground for tiny utilities used in 2+ packages without forming a deep hierarchy:

  • packages/shared/ReactSymbols.jsREACT_*_TYPE symbols.
  • packages/shared/ReactFeatureFlags.js — the central flag table.
  • packages/shared/ReactSharedInternals.js — the inter-package "back door" used by hooks/dispatcher.
  • packages/shared/ReactErrorUtils.js, ReactComponentStackFrame.js, forwardRefIcon.js, etc. — small utilities.

If a utility ends up shared across only the renderer and the reconciler, it usually goes in react-reconciler and is re-exported. If it's used across renderers and react, it belongs in shared.

"Don't break the world" reflexes

  • Adding a new fiber tag → update packages/react-reconciler/src/ReactWorkTags.js and the matching switch in getComponentNameFromFiber.js, plus packages/react-devtools-shared/src/backend/fiber/renderer.js (DevTools needs to know about it too).
  • Adding a new flag bit → update packages/react-reconciler/src/ReactFiberFlags.js and audit every commit-phase walk that sets/checks flags.
  • Adding a new built-in hook → wire it up in all dispatcher tables (mount, update, rerender, server) in packages/react-reconciler/src/ReactFiberHooks.js, plus the SSR-side dispatcher in packages/react-server/src/ReactFizzHooks.js, plus the dispatcher in packages/react-dom/src/server/.
  • Adding a new symbol type → register it in packages/shared/ReactSymbols.js and packages/shared/getComponentNameFromType.js.

These are the points where a "small" change can otherwise become a six-PR rollback.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – React wiki | Factory