React: the component model
Baseline: React 19.2 with React Compiler 1.0. See ../../STACK_BASELINE.md.
This is the entry point — what React is, how JSX and the component model work, and the hooks you use in every file. Depth lives in the siblings listed in README.md.
What React actually is
A library for describing UI as a function of state. You write components that return a description of what the screen should look like; React works out the minimal DOM operations to make reality match. You never write “find this node and change its text” — that is the whole point.
It is a library, not a framework. It has no router, no data layer, and no build system. Those come from the framework you put it in.
Starting a project
create-react-app is deprecated — the React team archived it and the docs no longer recommend it. As of 2026 the choices are:
| Tool | Use when |
|---|---|
| Vite | SPA, client-rendered, you own the backend separately |
| Next.js (App Router) | you want Server Components, SSR/SSG, and file-based routing |
| React Router (framework mode) | SSR and routing without the Next.js/Vercel ecosystem |
| Expo | React Native |
npm create vite@latest my-app -- --template react-ts
Reaching for a framework is the default answer for anything user-facing: SSR, streaming and Server Components are not things you retrofit onto a Vite SPA cheaply. See server_components.md.
Components
A component is a function taking props and returning JSX. That is the entire contract.
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}
Rules that are not optional:
- The name must be capitalised.
<greeting />is parsed as an HTML tag. - It must be pure during render: same props and state produce the same output, with no side effects. React may call it more than once, discard the result, or restart it — Strict Mode double-invokes in development to surface violations.
- Hooks are called unconditionally at the top level, never in a loop, condition, or nested function. React matches hooks to state by call order.
Class components still work, but they are legacy — no new API arrives for them, and Server Components, use(), and the compiler are function-only. See lifecycle.md for the mapping from lifecycle methods to effects.
JSX
Syntax sugar for React.createElement, compiled away at build time. It produces plain objects describing elements, not DOM nodes. See element_vs_component.md.
const el = <h1 className="title">Hi</h1>;
// compiles to a call producing { type: 'h1', props: { className: 'title', children: 'Hi' } }
Consequences worth knowing:
- Attributes use JS names:
className,htmlFor,onClick. {}embeds an expression, not a statement. Noif, nofor— use a ternary,&&, or.map().{cond && <X />}renders0whencondis0, because0is a valid React child. Usecond ? <X /> : nullwhen the value could be numeric.- A component returns one root. Use a fragment to avoid a wrapper div — see react_fragment.md.
Props and state
Props flow down and are read-only from the child’s perspective. State is owned by one component and changed only through its setter.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Use the updater form setCount(c => c + 1) whenever the next value depends on the current one. setCount(count + 1) reads a value captured at render time, so two updates in the same event handler collapse into one — the classic stale-closure bug. See stale_closures_and_hook_rules.md.
State updates are asynchronous and batched. Reading count immediately after calling setCount gives you the old value; that is by design, not a race to work around.
When two components need the same state, lift it to their nearest common parent. When that parent is far away, use context — but note that every consumer re-renders when the context value changes, which is its own problem (context_performance.md).
The hooks you use daily
| Hook | For |
|---|---|
useState |
local state |
useReducer |
local state with several related transitions |
useEffect |
synchronising with something outside React |
useRef |
a mutable box that does not trigger re-render, or a DOM handle |
useContext |
reading a value provided above |
use |
reading a promise or context, callable conditionally (19+) |
useEffect is the one people misuse. It is for synchronising with external systems — subscriptions, non-React widgets, browser APIs. It is not for deriving state from props (compute it during render) and not the recommended way to fetch data (use a framework loader, or a library like TanStack Query). Every effect needs a cleanup if it starts anything, because Strict Mode mounts, unmounts, and remounts in development specifically to catch missing cleanup. See use_effect_deep.md.
useMemo, useCallback and React.memo are deliberately absent from that table. With React Compiler 1.0 enabled, memoization is inserted automatically and hand-written memo calls are noise at best. See react_compiler.md and react_memo.md.
Interview angle
- “What does React actually do for you?” - it lets you describe UI declaratively as a function of state and handles the DOM updates. The follow-up is usually reconciliation: how it decides what changed. See reconciliation.md.
- “How would you start a React project today?” - Vite for an SPA, Next.js or React Router framework mode when you need SSR. Saying
create-react-appdates you immediately; it has been archived. - “Why must hooks be called unconditionally?” - React identifies each hook by call order within the component, not by name. A conditional hook shifts every subsequent hook’s identity and state gets read from the wrong slot.
- “Why does my counter only increment once when I click twice fast?” -
setCount(count + 1)closes over the render-time value. The updater form reads the latest state and composes correctly under batching.