React

Status: 🟩 COMPLETE (🟦 LIVING) Last updated: 2026-06-20 Plain-English tagline: A JavaScript library for building UIs out of small reusable components. You describe what the UI should look like for a given state; React figures out how to update the screen.


In plain English

Before React, building interactive web UIs meant manually updating the DOM (“now change this text,” “now hide that div,” “now insert this new element”). The code became a tangle of “what’s currently on screen” mixed with “what should change next.” Bugs everywhere.

React flipped the model: you describe what the UI should look like for the current data, and React figures out the updates. State changes → React re-renders → DOM updates. You stop thinking about transitions; you think about the final state.

A React component is a JavaScript function that returns markup (JSX). Small components compose into bigger ones. The whole UI is a tree of components. When data changes, React re-renders the affected components and efficiently updates the actual page.

Created at Facebook (now Meta) in 2013. Open-source. Now the dominant frontend library in the world. As of June 2026, React 19.2 is current, and the React Compiler 1.0 has stabilized.


Why it matters

  • It’s the foundation of Next.js, the framework you use.
  • Component-based UIs scale. A 100,000-line React codebase is no harder to navigate than a 1,000-line one because components compose.
  • The ecosystem is enormous. Tens of thousands of pre-built components (shadcn/ui, Radix, MUI, Mantine), patterns, tutorials.
  • Skills transfer. Most modern React knowledge applies to React Native (mobile), Remix, Astro, and dozens of other frameworks.
  • Job market. React dominates frontend hiring in 2026.

You can’t seriously do modern web development without at least a working knowledge of React.


The mental model in 4 sentences

  1. A component is a function that returns JSX (HTML-flavored markup).
  2. Components have state (data that can change) and props (data passed in from a parent).
  3. When state or props change, React re-renders the component — calls the function again to produce new JSX.
  4. React diffs the new JSX against the previous one and updates only the parts of the DOM that actually changed.

That’s the core. Everything else is detail.


A first component

function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}!</h1>;
}
 
// Usage:
<Greeting name="George" />
  • Function that takes one prop (name) and returns JSX.
  • JSX looks like HTML but with { curly braces } for embedding JavaScript expressions.
  • Used like an HTML tag with a capitalized name.

JSX — HTML-flavored JavaScript

const element = <h1 className="text-blue-500">Hello {name}</h1>;

Three key differences from HTML:

  1. class → className — because class is a reserved word in JavaScript.
  2. Self-closing tags must close. <br /> not <br>.
  3. Curly braces embed JS expressions: {name}, {2 + 2}, {user.name.toUpperCase()}.

You can also use any component as a JSX element:

<Card>
  <CardHeader title="Hello" />
  <CardBody>{children}</CardBody>
</Card>

JSX is compiled (by Next.js’s bundler, or Babel, or esbuild) into JavaScript function calls. <Greeting name="x" /> becomes roughly React.createElement(Greeting, { name: "x" }). You never write that directly; JSX is the developer experience.


Props — passing data down

Props are read-only inputs to a component:

interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary";
}
 
function Button({ label, onClick, variant = "primary" }: ButtonProps) {
  return (
    <button onClick={onClick} className={`btn btn-${variant}`}>
      {label}
    </button>
  );
}

Pass them like HTML attributes:

<Button label="Save" onClick={() => save()} variant="secondary" />

Props flow one direction — from parent down to children. A child can’t directly modify its parent’s data. (To go “up,” the parent passes a callback as a prop; the child calls it.)


State — data that changes over time

Use the useState hook:

import { useState } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  • useState(0) declares a piece of state initialized to 0.
  • Returns a tuple: the current value (count) and a setter (setCount).
  • Calling the setter triggers a re-render with the new value.

State is per-component instance. Two <Counter /> elements have independent counts.


Effects — running code when things change

Use the useEffect hook:

import { useEffect, useState } from "react";
 
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
 
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);  // re-run when userId changes
 
  if (!user) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

useEffect runs after the render. The dependency array [userId] tells React when to re-run:

  • [] — run once after first render
  • [userId] — run when userId changes
  • (omit array) — run after every render (rarely what you want)

In modern React + Next.js, you’ll use useEffect less than you used to — Server Components handle most data fetching now, and many things that needed effects are better done elsewhere.


The most-used hooks

HookPurpose
useStateComponent state
useEffectRun side effects after render
useRefMutable container that survives renders (or DOM ref)
useMemoCache an expensive computation across renders
useCallbackCache a function definition across renders
useContextRead from a Context (shared state)
useReducerState with reducer pattern (complex state)
useTransitionMark state updates as non-urgent

In React 19.2 with the React Compiler, useMemo and useCallback are mostly automated — the compiler inserts memoization where needed. Less manual optimization required.


Components, big picture

A typical React app is a tree:

<App>
  <Header>
    <Logo />
    <Nav>
      <NavLink />
      <NavLink />
    </Nav>
  </Header>
  <Main>
    <Sidebar />
    <Content>
      <Article />
      <Comments>
        <Comment />
        <Comment />
      </Comments>
    </Content>
  </Main>
  <Footer />
</App>

Each component is a function. Each one returns JSX, possibly using props passed from its parent and state of its own. Composition all the way down.


Server Components vs Client Components (Next.js App Router)

In modern Next.js, components default to Server Components (run on the server, no JS shipped to browser). Add "use client" at the top of a file for Client Components (run in the browser, can use state and events).

// Default — Server Component
export default async function Page() {
  const data = await db.query("SELECT * FROM posts");
  return <PostList posts={data} />;
}
// Client Component
"use client";
import { useState } from "react";
export function ToggleButton() {
  const [on, setOn] = useState(false);
  return <button onClick={() => setOn(!on)}>{on ? "On" : "Off"}</button>;
}

Use Server Components for data fetching, layout, anything static. Use Client Components for interactivity (state, events, effects). The split is covered in detail in Next.js.


Conditional rendering

function Message({ isLoggedIn }: { isLoggedIn: boolean }) {
  // Method 1: ternary
  return isLoggedIn ? <p>Welcome!</p> : <p>Please log in.</p>;
 
  // Method 2: && (short-circuit)
  return <>{isLoggedIn && <p>Welcome!</p>}</>;
 
  // Method 3: early return
  if (!isLoggedIn) return <p>Please log in.</p>;
  return <p>Welcome!</p>;
}

All three are common. Pick what reads best for the situation.


Lists

function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

map over the data, return JSX for each item. The key prop on each item is required — it helps React efficiently update the list when items change. Use a unique stable ID (not array index unless the list is static).


Events

function SearchBox() {
  const [query, setQuery] = useState("");
 
  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      onKeyDown={(e) => {
        if (e.key === "Enter") doSearch(query);
      }}
    />
  );
}

Event handlers are camelCase (onChange, not onchange). They receive a synthetic event object similar to the native browser event.


Forms (controlled inputs)

function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    login(email, password);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Log in</button>
    </form>
  );
}

“Controlled inputs” means React state holds the value; the DOM input mirrors it. Predictable and easy to validate.

Alternative: uncontrolled inputs with refs. Used less often.

In modern Next.js, Server Actions often replace this pattern entirely — <form action={serverAction}> with no client state.


Common gotchas

  • State updates are asynchronous and batched. setCount(count + 1) doesn’t update count immediately. If you read count on the next line, it’s still the old value. For derived updates: setCount(c => c + 1).

  • useEffect runs twice in development. React’s StrictMode intentionally double-runs effects to surface side-effect bugs. Production runs them once.

  • Missing dependencies in useEffect. If your effect uses a variable but doesn’t list it in the dependency array, you get stale closures. The ESLint plugin (react-hooks/exhaustive-deps) warns about this — listen to it.

  • The Rules of Hooks. Only call hooks at the top level of a component or another hook. Never inside conditionals, loops, or after returns. React relies on call order to track which hook is which.

  • Components capitalize. A custom component MUST start with a capital letter (<MyButton />). Lowercase tags (<button />) are treated as HTML elements.

  • key is required for lists. And it must be unique and stable. Array indexes work only if the list never reorders.

  • Don’t mutate state directly. state.x = 5 won’t trigger a re-render. Always use the setter: setState({ ...state, x: 5 }).

  • useState initial value is computed every render. Heavy initial computation should use the function form: useState(() => expensiveCompute()).

  • Closures and stale state. Inside useEffect or useCallback, the captured values can become stale. Use refs or the function form of setters to read latest state.

  • Re-rendering is cheap; DOM updates are cheap. Don’t prematurely optimize. The React Compiler now handles most memoization automatically.

  • Hydration mismatches. SSR-rendered HTML must exactly match what the client produces. Time, random values, and browser-only APIs cause mismatches.

  • Don’t fetch in useEffect if you can avoid it. In Next.js App Router, fetch in Server Components. They’re simpler, faster, and don’t ship JavaScript to the client.

  • useState lazy initialization. useState(expensive()) runs expensive() every render. useState(() => expensive()) runs it only on mount.


See also

  • Next.js đźź© 🟦 — the framework around React
  • JavaScript đźź© — the underlying language
  • TypeScript đźź© — pairs naturally with React
  • The DOM đźź© — what React updates
  • Tailwind CSS đźź© — styling React components
  • ui đźź© — pre-built React components
  • Glossary: React, JSX, Hook, Component

Sources