React
Components and JSX, props and state, hooks and effects, lists and keys, context, and the boundaries that stop one failure taking the page.
The One Idea
Before React, code mutated the DOM directly — find the node, change the text, toggle a class. That works until two places can change the same thing, at which point the screen and the data quietly disagree.
React inverts it. You never say how to update; you say what the output should be for the current state. Re-rendering is cheap because React compares the described output against the previous one and applies only the differences.
Learn one component model deeply and the rest transfer. Vue, Svelte and Angular solve the same problem with different syntax; the reasoning about state, derived values and effects is the same everywhere.
Components & JSX
A component is a function that takes an object of inputs and returns a description of some UI. JSX is syntax sugar over ordinary function calls — it compiles away entirely.
function Greeting({ name, muted }) { return <p className={muted ? 'dim' : ''}>Hello, {name}</p>; } // the compiler turns that into a plain function call: // jsx('p', { className: ..., children: ['Hello, ', name] }) // so it is JavaScript, and normal rules apply: {items.map(i => <Row key={i.id} {...i} />)} // loops are map {isOpen && <Panel />} // conditionals are expressions
Because it is JavaScript, there is no template language to learn — but the attribute names follow the DOM properties rather than HTML: className not class, htmlFor not for, and event handlers are camelCase functions rather than strings.
<greeting /> renders an unknown element and nothing happens — with no error.Props & State
Props come from the parent and are read-only. State belongs to the component and, when it changes, causes a re-render. That is the whole distinction, and most design mistakes are putting a value in the wrong one.
const [count, setCount] = useState(0); // wrong: both calls read the same stale count, so this adds 1 setCount(count + 1); setCount(count + 1); // right: the updater form sees the value React is about to use setCount(c => c + 1); setCount(c => c + 1); // adds 2 // never mutate — React compares by reference items.push(x); // same array, no re-render setItems([...items, x]); // new array, renders
State updates are asynchronous and batched: reading the variable straight after setting it gives the old value. That surprises everyone once, and the updater form is the answer whenever the next value depends on the previous one.
Do not put in state anything you can calculate during render. A fullName derived from first and last is not state — it is an expression. Storing it creates a second source of truth that can drift.
Hooks & Effects
Hooks let a function component hold state and reach outside itself. They must be called unconditionally at the top level — React tracks them by call order, so a hook inside an if shifts every hook after it.
| Hook | For |
|---|---|
useState | A value that changes and should re-render |
useEffect | Synchronising with something outside React |
useRef | A mutable box that does not re-render; DOM access |
useMemo | Caching an expensive calculation |
useCallback | Keeping a function's identity stable across renders |
useContext | Reading a value provided further up the tree |
useEffect is the most misused. It is for synchronising with an external system — a subscription, a timer, the document title, a network request. It is not a general "run this after render" hook, and reaching for it to compute derived data is the most common source of extra renders and stale values.
useEffect(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); // 1. cleanup }, [tick]); // 2. dependencies // no dependency array -> runs after every render // [] -> runs once on mount // [a, b] -> runs when a or b change
Lists, Keys & Reconciliation
When state changes, React builds a new description and compares it with the last. That comparison is reconciliation, and for lists it needs help identifying which item is which.
That is what key is for. It must be stable, unique and tied to the item — a database id, not an array index.
| Key | Result |
|---|---|
| A stable id | Correct: React moves the existing element |
| The array index | Breaks on insert, delete or reorder |
Math.random() | Every item remounts on every render |
| Omitted | Falls back to index, with a warning |
The index looks fine until you delete the first row. Every remaining item shifts index, so React thinks each one changed rather than moved — and any internal state, like what someone had typed into an input, lands on the wrong row.
Context & Composition
Passing a prop through five components that do not use it — prop drilling — is the problem context solves. A provider publishes a value; any descendant reads it without the layers between knowing.
Use it for genuinely global, rarely-changing things: the theme, the signed-in user, the language. It is not a state manager, and a context whose value changes often re-renders every consumer.
<Sidebar> element does not need to know what is inside it, so nothing has to be threaded through.Custom hooks are the other composition tool. Any function starting with use that calls other hooks is one, and it lets you share stateful logic — a fetch, a subscription, a form — between components without sharing markup.
Error Boundaries & Suspense
By default a thrown error during render unmounts the entire tree — the user gets a blank page. An error boundary catches errors from its children and renders a fallback instead, so one broken widget does not take the application.
Boundaries catch errors during rendering, in lifecycle methods and in constructors below them. They do not catch errors in event handlers, in asynchronous code, or in the boundary itself — those still need ordinary try/catch.
Suspense is the same idea for waiting rather than failing: it renders a fallback while something below it is not ready. Wrap the part of the page that depends on slow data, so the rest still paints.
Common Mistakes
| Mistake | What happens |
|---|---|
| Mutating state directly | Same reference, so no re-render |
| Array index as key | State lands on the wrong row after a reorder |
useEffect for derived data | Extra render, stale values, sometimes a loop |
| Missing effect cleanup | Duplicate timers and subscriptions |
| Reading state right after setting it | Gets the old value; updates are batched |
| Hook inside a condition | Call order shifts and hooks break |
| Everything in context | Every consumer re-renders on every change |
Interview Questions
Why must keys be stable and unique?
They tell React which item is which between renders. With array indexes, deleting or reordering shifts every index, so React treats moves as changes and any internal state — text typed into an input, for instance — ends up on the wrong item.
What is useEffect actually for?
Synchronising with something outside React: a subscription, a timer, the document title, a request. It is not a post-render hook, and using it to compute derived data causes an extra render and stale values.
Why does calling setCount(count + 1) twice only add one?
Both calls close over the same value from this render, and updates are batched. The updater form, setCount(c => c + 1), receives the pending value, so two calls add two.
Props or state?
Props come from the parent and are read-only. State is owned by the component and re-renders it when it changes. If a value can be computed from props or other state, it should not be state at all.
Why can hooks not go inside conditions?
React identifies hooks by call order, not name. A conditional hook changes that order between renders, so state from one hook is handed to another.
When would you reach for context?
Rarely-changing global values — theme, current user, locale. For prop drilling, try composition first; for frequently-changing shared state, context re-renders every consumer and a purpose-built store is better.
Quick Quiz
items.push(x) then setItems(items) does not re-render because…