React
A concept-first tour of React — components, props, state, effects, and hooks — each idea explained before the code.
React is a library for building user interfaces out of components: small, reusable pieces of UI that describe what the screen should look like for a given set of data. You write what you want rendered, and React keeps the actual DOM in sync as your data changes. The examples here use function components and hooks, the modern style, against React 19.
React on its own handles the UI layer. For routing, data fetching, and a production build, it's usually paired with a framework — see the Next.js guide, which builds directly on these concepts.
Components and JSX
A component is a function that returns markup. That markup is JSX — an HTML-like syntax that compiles to JavaScript. A component's name must start with a capital letter so React can tell it apart from a plain HTML tag.
function Greeting() {
return <h1>Hello, world</h1>;
}
// Use it like an HTML tag
function App() {
return <Greeting />;
}Props: passing data in
Props are the inputs to a component — read-only values passed from parent to child, just like attributes on an HTML element. They let you reuse one component with different data. A component should never modify its own props.
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}</h1>;
}
function App() {
return <Greeting name="Ada" />; // renders "Hello, Ada"
}That function signature looks like it says name twice for no reason, but the two halves do
completely different jobs:
{ name }is JavaScript destructuring. React always calls your component with a single object of props — here{ name: "Ada" }— and{ name }reaches into that object and pulls thenamefield out into its own variable. It's shorthand forfunction Greeting(props) { const name = props.name; … }.: { name: string }is a TypeScript type annotation. It isn't code that runs; it tells the type checker "this component expects a prop calledname, and it must be a string." If a caller forgets it or passes a number, you get an error before the app ever runs.
In plain JavaScript (no TypeScript) you'd drop the second half entirely and just write
function Greeting({ name }) {. So: the first name extracts the value, the second describes
its type.
State: data that changes over time
State is data a component owns and can change in response to interaction. Calling the
useState hook gives you the current value and a setter; calling the setter tells React to
re-render the component with the new value. Never reassign the variable directly — always use the
setter.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}The line const [count, setCount] = useState(0) packs a lot in, so read it both ways:
- Right side:
useState(0)sets the starting value to0and hands back a two-item array — the current value and a function that updates it. - Left side:
const [count, setCount] = …is array destructuring: it unpacks that two-item array into two named variables, in order. The names are your choice; the convention issomethingandsetSomething. Socountis the current value andsetCountis how you change it.
It's exactly equivalent to the longer form:
const state = useState(0);
const count = state[0]; // the current value
const setCount = state[1]; // the updater functionCalling setCount(count + 1) is what tells React "the value changed — re-render with the new
number." Assigning count = count + 1 directly would do nothing, because React wouldn't know
anything happened.
Handling events
Event handlers are functions you pass to JSX props like onClick or onChange. They receive a
React event object and typically update state, which triggers a re-render.
function Toggle() {
const [on, setOn] = useState(false);
return <button onClick={() => setOn((prev) => !prev)}>{on ? "ON" : "OFF"}</button>;
}The setOn((prev) => !prev) part deserves a beginner's pause. Instead of handing the setter a new
value, you can hand it a function that receives the latest state — here named prev — and
returns the next one. !prev is JavaScript's "not" operator flipping the boolean, so true
becomes false and back again. You could write setOn(!on) and it would usually work, but the
function form is the safe habit: it always builds on the current value, even when several updates
happen in quick succession.
Rendering lists
To render a collection, map an array to an array of elements. Give each element a stable, unique
key so React can track items efficiently across re-renders — use a real id, not the array index
where you can.
function PenguinList({ penguins }: { penguins: { id: number; species: string }[] }) {
return (
<ul>
{penguins.map((p) => (
<li key={p.id}>{p.species}</li>
))}
</ul>
);
}Conditional rendering
Because JSX is just JavaScript, you render different UI with ordinary expressions — a ternary for
either/or, or && to render something only when a condition holds.
function Status({ loading, count }: { loading: boolean; count: number }) {
return (
<div>
{loading ? <p>Loading…</p> : <p>{count} results</p>}
{count === 0 && <p>No matches.</p>}
</div>
);
}Side effects with useEffect
Rendering should be pure — it just computes UI from props and state. Anything that reaches
outside React (fetching data, subscriptions, timers, manual DOM work) is a side effect and
belongs in useEffect. The dependency array controls when it re-runs; return a cleanup function
to undo the effect.
import { useEffect, useState } from "react";
function Clock() {
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id); // cleanup on unmount
}, []); // empty deps → run once after first render
return <time>{now.toLocaleTimeString()}</time>;
}Custom hooks: reusing logic
When several components need the same stateful logic, extract it into a custom hook — a
function whose name starts with use that calls other hooks. It bundles behavior, not markup, so
each component gets its own independent state.
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = () => setOn((v) => !v);
return [on, toggle] as const;
}
function Panel() {
const [open, toggle] = useToggle();
return <button onClick={toggle}>{open ? "Hide" : "Show"}</button>;
}Two small pieces of that hook are easy to trip over:
initial = falseis a default parameter. If a caller writesuseToggle()with no argument,initialisfalse;useToggle(true)starts it switched on.return [on, toggle] as constreturns the value and its toggler as a pair — the same[value, updater]shapeuseStategives you, which is whyPanelcan destructure it withconst [open, toggle] = useToggle(). Theas constis a TypeScript hint. Without it, TypeScript would describe the returned array loosely as "a list of (boolean or function)" and lose track of which slot is which;as constlocks in the order and types soopenis known to be a boolean andtogglea function. In plain JavaScript you'd simply writereturn [on, toggle];.
Controlled forms
In a controlled input, React state is the single source of truth: the input's value comes
from state, and onChange writes back to it. This keeps the UI and your data perfectly in sync
and makes validation straightforward.
function NameField() {
const [name, setName] = useState("");
return (
<>
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>Hello, {name || "stranger"}</p>
</>
);
}The rules of hooks
Only call hooks (useState, useEffect, …) at the top level of a component or another hook —
never inside loops, conditions, or nested functions. React relies on hooks being called in the
same order every render. This is the one rule that trips up most newcomers.