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.
npm install react react-dom
Output: added react and react-dom to dependencies
pnpm add react react-dom
Output: added 2 packages, linked from store
yarn add react react-dom
Output: added react and react-dom
bun add react react-dom
Output: installed react and react-dom
deno add npm:react npm:react-dom
Output: added npm:react and npm:react-dom to import map
For TypeScript projects, add the types:
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/reacttogether withreact. - Semver is observed but breaking changes ride on majors; 18 → 19 changed
forwardRef(now optional —refis 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
reactalone — 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 asreact— npm/pnpm will warn loudly otherwise.react-native— iOS / Android rendererreact/jsx-runtime— used by the automatic JSX transform (default since React 17). No manualimport React from "react"required for JSX.react/jsx-dev-runtime— dev-mode JSX transform with source-map locationsprop-types(legacy) — runtime prop validation. Largely replaced by TypeScript. Still required for some older libraries.
Major framework / ecosystem companions:
next— full-stack React frameworkremix— React Router based SSR framework@tanstack/react-query— server-state caching@tanstack/react-router— type-safe routingzustand,jotai,redux-toolkit— state managementreact-hook-form,formik— form helpers@testing-library/react+vitest/jest— component testing
Alternatives
| Library | Trade-off |
|---|---|
vue | Reactivity via refs/reactive objects; single-file components. Comparable size and ecosystem (Vuex, Pinia, Nuxt). |
svelte | Compile-to-vanilla-JS. Tiny runtime, no virtual DOM. SvelteKit for SSR. |
solid | JSX 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. |
qwik | Resumability instead of hydration. Steeper mental model, drastically less client JS. |
lit | Web-components based. Standards-aligned, smaller community. |
htmx | Server-rendered HTML + tiny JS for interactivity. Different paradigm. |
Common gotchas
- 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. useEffectruns 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).- Missing
keywarnings. Lists rendered without uniquekey={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. reactandreact-domMUST be the same version. Mismatched versions surface as cryptic runtime errors ("Invalid hook call" or "Cannot read invariant"). Pin both inpackage.jsonand the lockfile.- 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. - 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'sreact/jsx-uses-react: offfor new projects. forwardRefis optional in React 19. Existing code withforwardRefstill works, but new code can passrefas a regular prop. Library authors maintaining cross-version compatibility need both styles.- 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. useMemo/useCallbackare 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.- JSX boolean props.
disabled={false}removes the attribute entirely, butdisabled="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.
// 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.
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.
"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.
"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>
);
}
// 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.
// 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:
- Tree-shakes React's dev-only branches.
- Replaces
process.env.NODE_ENV === "development"withfalse. - 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).
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:
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
| Target | Stack | Notes |
|---|---|---|
| Vercel | Next.js / Remix / Vite SPA | First-party Next.js host. Edge + serverless functions. |
| Cloudflare Pages | Next.js (via @cloudflare/next-on-pages), Remix (Cloudflare adapter), Vite SPA | Free unlimited bandwidth. See npm-wrangler. |
| Netlify | Any | Build hooks + edge functions. |
| AWS Amplify / SST / CDK | Next.js, custom | Full AWS power. Higher operational cost. |
| Static host (S3 / Nginx) | Vite / CRA SPA | Cheapest. SPA fallback: serve index.html for unknown paths. |
| React Native / Expo | Mobile | EAS Build for binaries, OTA for JS updates. |
Pin both packages identically
{
"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.memofor 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/useEffectinto 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, defeatingReact.memo. Hoist to a constant or auseMemo. - 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.
| From | To | Key changes |
|---|---|---|
react@16 | react@17 | Automatic JSX transform — no more import React from "react". Event delegation moved from document to root. Concurrent mode stabilised. |
react@17 | react@18 | ReactDOM.createRoot replaces ReactDOM.render. Concurrent rendering by default. Automatic batching. Strict mode double-invokes. Suspense for data fetching. |
react@18 | react@19 | Server components stable. Actions and useActionState / useFormStatus / useOptimistic. Refs as props (no more forwardRef). Removed legacy APIs (React.createClass shims, etc.). |
18 → 19 migration checklist:
- Upgrade
reactandreact-domtogether. Usenpm overrides(or pnpm'soverrides/ yarn'sresolutions) to dedupe. - Upgrade
@types/reactand@types/react-domin lockstep. They're versioned independently. - Run
tsc --noEmit— many type changes surface here. The biggest:useState's initial value typing tightened. - Replace deprecated APIs flagged by the codemod:
npx codemod react/19/migrate-to-new-jsx-transformand friends. - Search for
forwardRef— leave existing usage, but adopt refs-as-props for new code. - Audit
PropTypes— React 19 removed runtime PropTypes from the package. Library code that importsprop-typesfromreactbreaks; installprop-typesseparately or migrate to TS. - If on a framework, upgrade the framework first — Next.js 15+, Remix v3 / React Router 7+ — and follow their guides.
- 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.
| Setup | Pattern |
|---|---|
| Vite | Native ESM. Picks React's ESM build. |
| Next.js | Bundler handles both. Internal SWC compiler manages JSX. |
| Webpack 5 | Both work; mainFields defaults to ESM first. |
| esbuild | ESM native. |
| tsx / ts-node | Works 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. |
| Bun | First-class. Built-in JSX transform. |
| Deno | import React from "npm:react" works. JSX requires --jsx-import-source react or a deno.json setting. |
| Cloudflare Workers | React for server rendering works (react-dom/server). Bundle size matters — react-dom is large; consider react-dom/server.edge. |
| React Native / Expo | Metro 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.
| Package | Role |
|---|---|
next, remix, react-router v7+ | Full-stack frameworks with SSR and routing. |
vite + @vitejs/plugin-react | SPA build setup with HMR. |
@tanstack/react-query | Server-state caching, mutations, optimistic updates. The standard for "data fetching with cache". |
@tanstack/react-router | Type-safe routing, file-based or code-defined. |
@tanstack/react-table | Headless data table primitives. |
redux-toolkit + react-redux | Classic global state, modern API. |
zustand | Tiny global state with hooks API. |
jotai | Atomic global state. |
react-hook-form | Form library with minimal re-renders. |
formik | Older form library — react-hook-form is the modern default. |
zod, valibot, yup | Schema validation. Pair with react-hook-form's resolvers. |
@testing-library/react + vitest / jest | Component testing. |
playwright, cypress | E2E browser testing. |
storybook | Component dev environment and visual regression. |
framer-motion, react-spring, @react-spring/web | Animation libraries. |
react-aria, radix-ui, headlessui, ariakit | Accessible primitive components. |
shadcn/ui | Copy-paste Tailwind components on top of Radix. |
mantine, chakra-ui, mui | Full component libraries. |
react-error-boundary | Error-boundary component for hook-only codebases. |
react-helmet-async, @unhead/react | <head> management for SPAs. |
i18next-react, react-intl, lingui | i18n. |
Testing & CI integration
Component test with @testing-library/react and Vitest
// 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):
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: { environment: "jsdom", setupFiles: ["./vitest.setup.ts"] },
});
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
E2E with Playwright
// 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
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
dangerouslySetInnerHTMLis XSS by default. React deliberately makes raw HTML insertion verbose. Sanitise withDOMPurifybefore 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}. Ajavascript:URL in a user-controlled link is XSS. Validate protocols withnew URL(href).protocol === "https:".src={userInput}for images. Same risk —javascript:anddata:URIs can be hostile. Allowlist origins.- Server-component leakage. Server components run with full server env. A bug that returns
process.envto the client serialises secrets into HTML. Be deliberate about what crosses the server/client boundary. useEffectfor auth. Auth checks inuseEffectrun 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 Action — useFormStatus 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
- JavaScript: react-basics — components, hooks, JSX in TypeScript
- JavaScript: astro — React islands inside an Astro project
- Concept: api — how React server actions and tRPC reshape API boundaries