cheat sheet

react

Package-level reference for the React library on npm — install, paired react-dom, server-component model, and upgrade gotchas.

react

What it is

react is the JavaScript library for building user interfaces using function components, hooks, and a virtual DOM. It is the rendering core only — actually painting to the browser or a native platform happens through a companion package (react-dom for the web, react-native for mobile, etc.). React is maintained by Meta with significant community contribution.

Reach for react when joining the largest UI ecosystem (Next.js, Remix, React Native, Astro islands, Expo). Reach for solid for finer-grained reactivity without a virtual DOM, svelte for compile-time reactivity, preact for a 3 kB API-compatible alternative, or vue for a different mental model with similar size.

Install

React always ships paired with a renderer. For browser apps install react AND react-dom at the same version.

bash
npm install react react-dom

Output: added react and react-dom to dependencies

bash
pnpm add react react-dom

Output: added 2 packages, linked from store

bash
yarn add react react-dom

Output: added react and react-dom

bash
bun add react react-dom

Output: installed react and react-dom

bash
deno add npm:react npm:react-dom

Output: added npm:react and npm:react-dom to import map

For TypeScript projects, add the types:

bash
npm install -D @types/react @types/react-dom

Output: added @types/react and @types/react-dom to devDependencies

Versioning & Node support

Current line is react@19.x (released late 2024). React 19 introduced stable server components, the React Compiler, server actions, and the use hook.

  • React itself runs anywhere JavaScript runs — Node, browsers, Deno, Bun.
  • For SSR / server components, Node 18.18+ or 20+ is the common floor (specific framework adapters may demand more).
  • Dual ESM/CJS, but ESM is the modern path.
  • TypeScript types live in @types/react (DefinitelyTyped) — not in-tree. Always upgrade @types/react together with react.
  • Semver is observed but breaking changes ride on majors; 18 → 19 changed forwardRef (now optional — ref is a regular prop), removed several legacy APIs, and adjusted concurrent-mode behaviour.

Package metadata

  • Maintainer: Meta Open Source + React core team
  • Project home: github.com/facebook/react
  • Docs: react.dev
  • npm: npmjs.com/package/react
  • License: MIT
  • First released: 2013
  • Downloads: ~30 million weekly downloads for react alone — the most-used UI library on npm.

Peer dependencies & extras

react itself is a peer-dep target. Most React-related libraries declare react (and often react-dom) as a peer dependency rather than a direct dependency.

Closely-paired packages:

  • react-dom — DOM renderer. Always at the same version as react — npm/pnpm will warn loudly otherwise.
  • react-native — iOS / Android renderer
  • react/jsx-runtime — used by the automatic JSX transform (default since React 17). No manual import React from "react" required for JSX.
  • react/jsx-dev-runtime — dev-mode JSX transform with source-map locations
  • prop-types (legacy) — runtime prop validation. Largely replaced by TypeScript. Still required for some older libraries.

Major framework / ecosystem companions:

  • next — full-stack React framework
  • remix — React Router based SSR framework
  • @tanstack/react-query — server-state caching
  • @tanstack/react-router — type-safe routing
  • zustand, jotai, redux-toolkit — state management
  • react-hook-form, formik — form helpers
  • @testing-library/react + vitest / jest — component testing

Alternatives

LibraryTrade-off
vueReactivity via refs/reactive objects; single-file components. Comparable size and ecosystem (Vuex, Pinia, Nuxt).
svelteCompile-to-vanilla-JS. Tiny runtime, no virtual DOM. SvelteKit for SSR.
solidJSX with fine-grained reactivity (no virtual DOM). Very small bundle, near-React API.
preact~3 kB React-compatible. Same JSX, fewer batteries. Used inside Astro and Cloudflare's Workers Sites starter.
qwikResumability instead of hydration. Steeper mental model, drastically less client JS.
litWeb-components based. Standards-aligned, smaller community.
htmxServer-rendered HTML + tiny JS for interactivity. Different paradigm.

Common gotchas

  1. React 18 → 19 server components changed the mental model. Components are now server by default in supported frameworks (Next.js App Router, Remix v3 / React Router 7). The "use client" directive marks the boundary. Hooks (useState, useEffect) only run in client components — calling them in a server component is a hard error.
  2. useEffect runs twice in dev under StrictMode. React mounts → unmounts → remounts every component in strict mode to surface side-effect bugs. Production runs each effect once. Code that "works in prod but breaks in dev" usually has a non-idempotent effect (e.g. incrementing a counter, opening a socket).
  3. Missing key warnings. Lists rendered without unique key={id} cause incorrect reuse during reconciliation. Index-as-key is fine ONLY when the list is append-only and never reordered — otherwise input focus / form state migrate to the wrong row.
  4. react and react-dom MUST be the same version. Mismatched versions surface as cryptic runtime errors ("Invalid hook call" or "Cannot read invariant"). Pin both in package.json and the lockfile.
  5. Concurrent rendering can interrupt + restart components. Renders are no longer guaranteed to run to completion before the next one starts. State setters during render that don't bail out cause infinite render loops. Always wrap effectful work in useEffect / useTransition.
  6. Old import React from "react" is unnecessary. Since the automatic JSX transform shipped (React 17), the JSX runtime handles imports. Linters may still complain — configure ESLint's react/jsx-uses-react: off for new projects.
  7. forwardRef is optional in React 19. Existing code with forwardRef still works, but new code can pass ref as a regular prop. Library authors maintaining cross-version compatibility need both styles.
  8. Suspense boundaries are inheritance-based. A child component that suspends bubbles up to the nearest <Suspense> — if no boundary exists, the entire app falls back. Wrap data-fetching components defensively.
  9. useMemo / useCallback are not always optimisations. The new React Compiler (rolled out gradually) auto-memoizes; manual memoization may become net negative once the compiler ships everywhere. Profile before adding.
  10. JSX boolean props. disabled={false} removes the attribute entirely, but disabled="false" (string) is truthy and disables the element. TypeScript usually catches this; plain JSX doesn't.

Real-world recipes

Server component data fetching (React 19)

Server components fetch on the server and stream HTML to the client. No useEffect round-trip. This pattern only works inside a framework that hosts React server components — Next.js App Router and Remix v3 / React Router 7 are the canonical hosts.

tsx
// app/users/page.tsx — server component (no "use client")
async function getUsers() {
  const res = await fetch("https://api.example.com/users", { next: { revalidate: 60 } });
  return res.json() as Promise<{ id: string; name: string }[]>;
}

export default async function UsersPage() {
  const users = await getUsers();
  return (
    <ul>
      {users.map((u) => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

Output: server fetches once per minute (Next's revalidation), streams the rendered list to the browser; the client never imports fetch for this data.

Suspense + error boundary for streamed data

Wrap any component that suspends so the fallback UI is local, not page-wide.

tsx
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";

export default function Dashboard() {
  return (
    <ErrorBoundary fallback={<p>Something went wrong</p>}>
      <Suspense fallback={<p>Loading users…</p>}>
        <UsersList />
      </Suspense>
    </ErrorBoundary>
  );
}

Output: UsersList streams in independently; if it throws, only that subtree shows the error.

useTransition for slow updates

When a state change triggers a heavy render (large list, table sort, search), wrap the setter in useTransition so the UI stays responsive.

tsx
"use client";
import { useState, useTransition } from "react";

export function Search({ items }: { items: string[] }) {
  const [query, setQuery] = useState("");
  const [filtered, setFiltered] = useState(items);
  const [isPending, startTransition] = useTransition();

  function update(q: string) {
    setQuery(q);
    startTransition(() => {
      setFiltered(items.filter((i) => i.includes(q)));
    });
  }

  return (
    <>
      <input value={query} onChange={(e) => update(e.target.value)} />
      {isPending && <p>Updating…</p>}
      <ul>{filtered.map((i) => <li key={i}>{i}</li>)}</ul>
    </>
  );
}

Output: the input remains snappy even when filtering 10,000 items; pending UI shows during the transition.

Server action with useFormStatus and useOptimistic

React 19's Actions + useOptimistic give you instant UI feedback while the server processes.

tsx
"use client";
import { useOptimistic } from "react";
import { addTodo } from "./actions";

export function TodoList({ todos }: { todos: { id: string; text: string }[] }) {
  const [optimistic, addOptimistic] = useOptimistic(
    todos,
    (state, newText: string) => [...state, { id: "temp", text: newText }],
  );

  async function action(formData: FormData) {
    const text = String(formData.get("text"));
    addOptimistic(text);
    await addTodo(text);
  }

  return (
    <form action={action}>
      <input name="text" />
      <button type="submit">Add</button>
      <ul>{optimistic.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
    </form>
  );
}
tsx
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";

export async function addTodo(text: string) {
  await db.todos.insert({ text });
  revalidatePath("/todos");
}

Output: the new todo appears instantly; if the server action fails, React rolls back.

Ref-as-prop migration (no more forwardRef)

React 19 lets functional components accept ref as a regular prop. Existing forwardRef code continues to work; new code is shorter.

tsx
// React 19 — no forwardRef
function MyInput({ ref, ...props }: React.InputHTMLAttributes<HTMLInputElement> & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

// Pre-19 equivalent — kept for cross-version libraries
import { forwardRef } from "react";
const MyInputLegacy = forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
  (props, ref) => <input ref={ref} {...props} />,
);

Output: same DOM and ref semantics; the new form is plain destructuring.

Production deployment

React itself is platform-agnostic — the deployment story belongs to the framework wrapping it (Next.js, Remix, Astro, Vite SPA, React Native, Expo). The React-specific concerns are bundle size, hydration cost, and version pinning.

Production build

Every framework has a build command that:

  1. Tree-shakes React's dev-only branches.
  2. Replaces process.env.NODE_ENV === "development" with false.
  3. Minifies output.

The result is dramatic: React's dev build is ~140 KB, prod is ~45 KB minified, ~13 KB gzipped (numbers vary by minor — always measure in your actual bundle).

bash
NODE_ENV=production npm run build

Output: framework-specific. Vite emits dist/, Next.js emits .next/.

Bundle splitting

Code-split route components with React's lazy() + Suspense:

tsx
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./Dashboard"));

export default function App() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <Dashboard />
    </Suspense>
  );
}

Output: Dashboard is fetched as a separate chunk on first render. The bundler emits the chunk automatically.

Deployment targets

TargetStackNotes
VercelNext.js / Remix / Vite SPAFirst-party Next.js host. Edge + serverless functions.
Cloudflare PagesNext.js (via @cloudflare/next-on-pages), Remix (Cloudflare adapter), Vite SPAFree unlimited bandwidth. See npm-wrangler.
NetlifyAnyBuild hooks + edge functions.
AWS Amplify / SST / CDKNext.js, customFull AWS power. Higher operational cost.
Static host (S3 / Nginx)Vite / CRA SPACheapest. SPA fallback: serve index.html for unknown paths.
React Native / ExpoMobileEAS Build for binaries, OTA for JS updates.

Pin both packages identically

json
{
  "dependencies": {
    "react": "19.0.0",
    "react-dom": "19.0.0"
  },
  "overrides": {
    "react": "19.0.0",
    "react-dom": "19.0.0"
  }
}

The overrides block (npm 8+) forces transitive deps to use the same version, preventing the "two copies of React" runtime error.

Performance tuning

React 19's compiler auto-memoizes much of what useMemo / useCallback used to do manually. Until the compiler is ubiquitous, the historical advice still applies.

  • Profile first. React DevTools' Profiler shows render durations and what triggered each render. Optimise based on data, not vibes.
  • Move state down. A state setter at the root re-renders the whole tree. Pushing state into the leaf that needs it limits the blast radius.
  • React.memo for expensive pure components. Bails out of rerender if props are referentially equal. Don't blanket-apply — the comparison cost can outweigh the savings.
  • useDeferredValue. Lets React render a stale value while a fresh one is being prepared. Good for input → list-filter UIs.
  • useTransition. Mark state updates as non-urgent so input handling stays interactive.
  • List virtualisation (react-virtuoso, @tanstack/react-virtual). Rendering 10,000 rows is slow even when each row is cheap. Virtualise.
  • Suspense for data fetching. Move loading states from useState / useEffect into Suspense boundaries — React can render the rest of the page while data streams.
  • Avoid inline object/array props. <X data={{ id: 1 }} /> creates a new object each render, defeating React.memo. Hoist to a constant or a useMemo.
  • Server-component budget. Server components are free on the client but cost CPU on the server. Cache, revalidate, or stream — don't fetch the same data per render.
  • Hydration cost. Each interactive island has hydration overhead. Smaller islands hydrate faster. In Astro / Next App Router, push interactivity to leaf components.

A useful rule of thumb: a render that completes in <16 ms feels instant. The Profiler highlights anything slower.

Version migration guide

React's majors are infrequent (17 → 18 → 19) and well-documented. The 18 → 19 migration is the most recent and the most significant in years. Always consult the official upgrade guide for your specific version pair, since the platform changes between minor releases too.

FromToKey changes
react@16react@17Automatic JSX transform — no more import React from "react". Event delegation moved from document to root. Concurrent mode stabilised.
react@17react@18ReactDOM.createRoot replaces ReactDOM.render. Concurrent rendering by default. Automatic batching. Strict mode double-invokes. Suspense for data fetching.
react@18react@19Server components stable. Actions and useActionState / useFormStatus / useOptimistic. Refs as props (no more forwardRef). Removed legacy APIs (React.createClass shims, etc.).

18 → 19 migration checklist:

  1. Upgrade react and react-dom together. Use npm overrides (or pnpm's overrides / yarn's resolutions) to dedupe.
  2. Upgrade @types/react and @types/react-dom in lockstep. They're versioned independently.
  3. Run tsc --noEmit — many type changes surface here. The biggest: useState's initial value typing tightened.
  4. Replace deprecated APIs flagged by the codemod: npx codemod react/19/migrate-to-new-jsx-transform and friends.
  5. Search for forwardRef — leave existing usage, but adopt refs-as-props for new code.
  6. Audit PropTypes — React 19 removed runtime PropTypes from the package. Library code that imports prop-types from react breaks; install prop-types separately or migrate to TS.
  7. If on a framework, upgrade the framework first — Next.js 15+, Remix v3 / React Router 7+ — and follow their guides.
  8. Profile production. Concurrent semantics may surface re-renders that the eager 16-style renderer hid.

ESM/CJS interop & bundling

React itself dual-publishes ESM and CJS. The bundler usually picks the right one transparently.

SetupPattern
ViteNative ESM. Picks React's ESM build.
Next.jsBundler handles both. Internal SWC compiler manages JSX.
Webpack 5Both work; mainFields defaults to ESM first.
esbuildESM native.
tsx / ts-nodeWorks under ESM and CJS. Set "jsx": "react-jsx" (automatic transform) or "jsx": "preserve" (if the bundler handles JSX).
TypeScript"jsx": "react-jsx" enables the automatic transform; "jsx": "react" uses the legacy React.createElement form.
BunFirst-class. Built-in JSX transform.
Denoimport React from "npm:react" works. JSX requires --jsx-import-source react or a deno.json setting.
Cloudflare WorkersReact for server rendering works (react-dom/server). Bundle size matters — react-dom is large; consider react-dom/server.edge.
React Native / ExpoMetro bundler manages its own resolution. React + React Native share the React core but use a different renderer.

The most common interop bug: two copies of React in the bundle (one from your dep tree, one from a sub-dep). Symptoms: "Invalid hook call" at runtime. Fix with overrides / resolutions and verify with npm ls react.

Plugin & ecosystem coverage

React has the largest UI library ecosystem on npm. The shortlist below covers the categories most apps need.

PackageRole
next, remix, react-router v7+Full-stack frameworks with SSR and routing.
vite + @vitejs/plugin-reactSPA build setup with HMR.
@tanstack/react-queryServer-state caching, mutations, optimistic updates. The standard for "data fetching with cache".
@tanstack/react-routerType-safe routing, file-based or code-defined.
@tanstack/react-tableHeadless data table primitives.
redux-toolkit + react-reduxClassic global state, modern API.
zustandTiny global state with hooks API.
jotaiAtomic global state.
react-hook-formForm library with minimal re-renders.
formikOlder form library — react-hook-form is the modern default.
zod, valibot, yupSchema validation. Pair with react-hook-form's resolvers.
@testing-library/react + vitest / jestComponent testing.
playwright, cypressE2E browser testing.
storybookComponent dev environment and visual regression.
framer-motion, react-spring, @react-spring/webAnimation libraries.
react-aria, radix-ui, headlessui, ariakitAccessible primitive components.
shadcn/uiCopy-paste Tailwind components on top of Radix.
mantine, chakra-ui, muiFull component libraries.
react-error-boundaryError-boundary component for hook-only codebases.
react-helmet-async, @unhead/react<head> management for SPAs.
i18next-react, react-intl, linguii18n.

Testing & CI integration

Component test with @testing-library/react and Vitest

typescript
// Counter.test.tsx
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import Counter from "./Counter";

describe("Counter", () => {
  it("increments on click", () => {
    render(<Counter />);
    fireEvent.click(screen.getByRole("button"));
    expect(screen.getByText(/Clicks: 1/)).toBeInTheDocument();
  });
});

Vitest config (vitest.config.ts):

typescript
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  test: { environment: "jsdom", setupFiles: ["./vitest.setup.ts"] },
});
typescript
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";

E2E with Playwright

typescript
// e2e/login.spec.ts
import { test, expect } from "@playwright/test";

test("user can log in", async ({ page }) => {
  await page.goto("/login");
  await page.fill('[name="email"]', "alice@example.com");
  await page.fill('[name="password"]', "secret");
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL("/dashboard");
});

CI pipeline

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npm test -- --run
      - run: npm run build
      - run: npx playwright install --with-deps
      - run: npx playwright test

Security considerations

  • dangerouslySetInnerHTML is XSS by default. React deliberately makes raw HTML insertion verbose. Sanitise with DOMPurify before using. Never pass untrusted user content directly.
  • Hydration mismatches as a security signal. A mismatch between server and client render can hide auth-state confusion (rendering "Welcome, Alice" on the server but client expects logged-out state). Treat mismatches as bugs, not warnings.
  • Server actions are public endpoints. Marking a function "use server" doesn't authenticate it — anyone can POST to the generated URL. Validate the caller's session inside every action.
  • href={userInput}. A javascript: URL in a user-controlled link is XSS. Validate protocols with new URL(href).protocol === "https:".
  • src={userInput} for images. Same risk — javascript: and data: URIs can be hostile. Allowlist origins.
  • Server-component leakage. Server components run with full server env. A bug that returns process.env to the client serialises secrets into HTML. Be deliberate about what crosses the server/client boundary.
  • useEffect for auth. Auth checks in useEffect run AFTER the client renders the protected content for a frame. Use a server component or middleware to gate access.
  • CSP and inline styles. React injects inline styles for style={...} props. CSP requires 'unsafe-inline' or nonces. Modern Next.js handles nonces automatically; bare React + Vite needs manual config.
  • CSRF for actions. Server actions are POST-only and bound to your origin via SameSite cookies, but a custom action URL can still be CSRF'd if cookies are SameSite=None. Use double-submit tokens for sensitive operations.

Troubleshooting common errors

Invalid hook call — almost always two copies of React. Run npm ls react and dedupe via overrides.

Cannot read properties of null (reading 'useState') — same as above. React resolved to a different copy than the one with the renderer.

Hydration error: "Text content does not match server-rendered HTML" — locale formatting, dates, or random IDs differ between server and client. Pin the locale, use Intl with explicit options, or wrap problematic code in useEffect.

Error: useFormStatus must be used inside a form ActionuseFormStatus only reads state inside the descendant tree of a <form> with an action={fn} prop. Restructure the component.

Server component throws "You're importing a Component that needs useState" — adding interactivity in a server component. Add "use client" at the top of the file (or split into a client subcomponent).

<form action={fn}> doesn't work — only React 19+ supports passing functions to action. Earlier versions require a URL string. Check the React major.

Each child in a list should have a unique "key" prop" — list items missing key={id} or using key={index} after reordering. Use stable IDs.

Maximum update depth exceeded — a setState inside render runs every render. Move it to useEffect or gate with a condition.

useEffect runs twice in dev only — StrictMode. Confirm the effect is idempotent (no double-subscribing, double-mutating). It's a feature, not a bug.

ReactDOMServer.renderToString warning about Suspense — Suspense for data fetching only works with renderToPipeableStream / renderToReadableStream, not renderToString.

Stale closure in event handler — function reads a state variable from an old render's scope. Use the functional setter form (setN(n => n + 1)) or pull from a ref.

When NOT to use this

Skip React when:

  • The site is static or near-static. Content-heavy sites (docs, blogs, marketing) ship better with Astro / Eleventy / Hugo / 11ty — React's hydration cost is wasted.
  • Bundle size dominates. A widget on a third-party site must be <10 KB. Preact (~3 KB) is API-compatible. Lit / vanilla custom elements are smaller still.
  • Compile-time reactivity is acceptable. Svelte compiles away the runtime — no virtual DOM, smaller bundle. Solid uses fine-grained signals — closer to Svelte's perf without a compiler.
  • Resumability matters more than hydration. Qwik defers JS download until interaction. For brochureware with deep funnels, this can dramatically improve TTI.
  • The team is Vue / Angular invested. React's ecosystem is biggest but switching frameworks mid-project is rarely worth it.
  • No JS at all. htmx, Alpine, or plain server-rendered HTML can produce highly interactive UIs with no React. The "everything is a React app" reflex isn't universal best practice.
  • Native mobile. React Native works but Swift / Kotlin (or Flutter, or Expo) may fit better depending on the team.
  • The renderer doesn't exist. Server-side rendering to PDF, embedded systems, terminal UIs — react-pdf, ink, etc. exist but the maintenance trade-offs differ. Plain libraries for those targets can be simpler.

See also