CHAPTER 06 · FRONTEND
Thinking in React
React's API is small. What's hard is the change in thinking: you no longer "modify the UI"; you "describe what the UI should look like for a given state". Master that model and a few re-render rules, and you'll see where agent-written components are redundant and where they'll break.
- Explain the full flow after a click: event → state update → component functions run again → diff → DOM update.
- Predict which components re-render after a given state change.
- Decide whether a
useEffectis necessary and name three common alternatives. - Choose the right "home" for any piece of data: local state, lifted state, URL, server cache, global store.
Opening: two puzzling behaviors
Behavior one: you write let count = 0 in a component and count++ on button click. Logs show count increasing, but the number on screen stays 0.
Behavior two: an agent wrote a component that fetches the bookmark list in useEffect. In production, the browser hammers the same endpoint over and over.
Both come from the same mechanism. Which one?
Where the answer lies: a React component is a function that gets called again and again. Each "render" is one more call. Plain local variables are re-initialized on every call, and changing one doesn't trigger a new call; only "state" held by React survives across calls and triggers a re-run when it changes. And an effect runs conditionally after each render — if it changes state itself, and its dependency condition "looks changed" every time, you get the loop "render → effect → set state → render".
The core model: UI = f(state)
If you've written server templates (JSP, Jinja, Go templates), a component is a lot like a template: data in, HTML out. The difference: a server template renders once; React's "template" is automatically re-run when data changes, and only the differences are applied to the page, without a full reload.
Where it breaks: templates can often run side effects like queries freely; React requires component functions to be pure (same inputs → same output, no changes to the outside world), because React may call them at any time, any number of times.
Components, JSX and props
- JSX looks like HTML but compiles to ordinary function calls (roughly
jsx("li", { className: ..., children: ... })) that produce a plain JS object describing the UI. So you can put any expression inside{}and useifandmapfor conditions and loops — no template language. - props are arguments a parent passes to a child; read-only for the child. Data always flows downward.
keytells React "which item is which" in a list. It must be stable and unique (use the DB id). Using the array index as the key causes state mix-ups on insert, delete or sort (e.g. input text jumping to another row).
State: why plain variables don't work Deep
On click: plain++ changes a local variable in the current call but triggers nothing; setCount tells React "state changed", React schedules a new render and calls Counter() again. In the new call, plain starts at 0 again, while useState returns the new value 1 that React stored.
Every render is a "snapshot"
This is the biggest trap, and it's Chapter 03's closures showing up in React:
setCount doesn't change the count variable — it's a const belonging to this render's snapshot. The new value only appears in the snapshot of the next render.
Objects and arrays: always create new ones
Recall Chapter 03: React compares references to decide whether state changed.
Who re-renders? Deep
Here is the component tree of a LinkNote page. Click the buttons to change state in different places and watch which component functions run again (badge counters):
Three rules fall out of the demo:
- Re-rendering starts where the state lives. Its parents and siblings are unaffected.
- By default, when a component re-renders, every child it renders re-renders too, whether or not their props changed. That's usually fine — re-running functions is cheap, and React only writes real differences to the DOM.
memolets a child skip rendering when its props haven't changed. The React Compiler (1.0 released October 2025) does similar memoization automatically at build time, so new projects rarely need hand-writtenuseMemo,useCallbackormemo.
Two key design principles
Lifting state up: when two components need the same state (say SearchBox's input must filter List), move the state to their nearest common parent and pass it down via props — the value to one, the setter to the other.
If you can compute it, don't store it (derived state): the most common redundancy in agent code.
Effects: syncing with the outside world Deep
If component functions must be pure, what about subscribing to a WebSocket, calling browser APIs, or setting timers? That's what useEffect is for: after the render is committed to the screen, keep the component in sync with some external system.
Execution rules: run after the first render; after each later render, compare every item in the dependency array (with Object.is, i.e. by reference); if any changed, run the previous cleanup first, then the new effect.
How the infinite loop forms
The model: render → effect runs → setState → render → dependency "changed" → effect runs → …. Putting objects, arrays or functions created during render into the dependency array is the most common cause.
Under StrictMode in development, React deliberately mounts → unmounts → remounts a component once to expose effects missing a cleanup. If you see a request fire twice, don't rush to "fix" it — it's intentional and doesn't happen in production.
You might not need an effect
React's docs have a whole page called "You Might Not Need an Effect". By scenario:
| You want to | Don't | Do |
|---|---|---|
| Compute a value from props/state | Effect + setState | Compute during render |
| Respond to a user action (submit, click) | Set a flag and have an effect watch it | Do it in the event handler |
| Fetch data from the server | Hand-written effect + fetch | Framework loaders / Server Components / TanStack Query |
| Reset state when a prop changes | setState in an effect | Give the component a new key |
| Subscribe to an external system (WebSocket, browser events) | — | This is what effects are actually for |
Data fetching: don't hand-roll it Use
Hand-written useEffect + fetch looks simple, but you'd also need loading state, error state, races (switch filters quickly and an older request returns after a newer one, overwriting fresh data), caching, dedupe, retries, refetch on window focus… That's exactly what TanStack Query solves:
The idea behind it is worth remembering: server data isn't your "state"; it's a "cache" of remote data. What you care about is "when does it go stale and when is it refetched", not "how do I put it into useState". With full-stack frameworks, much of this fetching moves to server components or route loaders (Chapter 07); the idea is the same.
Forms and React 19 Actions Use
React 19 brings form handling back to how HTML works natively: pass a function to <form action> and React handles submission, pending state and errors.
Related hooks: useOptimistic (show the result before the server confirms, roll back on failure — an "optimistic update") and useFormStatus. In frameworks like Next.js, the action can be a function that runs on the server (a Server Action / Server Function, Chapters 07 and 08).
Two terms: a controlled input has its value driven by state, calling setState on every keystroke — for live validation or dependent fields; an uncontrolled input lets the browser keep the value and you read it from FormData on submit — simpler, fine for most forms.
Rules of hooks and custom hooks
Every function starting with use is a hook. Two hard rules: only call them at the top level of a component or custom hook (not inside ifs, loops or nested functions); only call them from React functions. React identifies which state belongs to which hook by call order. The react-hooks lint rule checks this automatically.
A custom hook bundles several hook calls into a function to reuse logic (not the state itself — each caller gets its own):
Where should data live?
"State management" is the most-discussed topic in the React world. In practice, if you ask these questions in order, you usually don't need a state library at all:
| Nature of the data | Where it lives | LinkNote example |
|---|---|---|
| Used by one component only | Local useState | Whether a dropdown is open |
| Shared by a few neighbors | Lifted to the common parent | Search query and the result list |
| Should survive refresh and be shareable | URL query params | Current tag filter, page number |
| Comes from the server | Server cache (TanStack Query / framework loaders) | Bookmark list, user profile |
| Read deep in the tree, rarely changes | Context | Theme, current user |
| Complex, cross-page, purely client-side | A store such as Zustand | Offline drafts, complex editor state |
Many people think they need "global state" when what they actually need is server cache (data from the backend) or URL state (filters, pagination, selected tab). Once those two are split off, very little client state remains.
List page: tag filter in the URL (?tag=react); the bookmark list in the server cache; search input as local state + debounce; "add link" as a form Action with optimistic update; the live "summary generating" progress via an effect subscribing to SSE (a genuine use of effects).
tag so it survives refresh and shared links. Compute the filtered result during render — don't store it in separate state or sync it with useEffect. Keep using the existing useQuery for the list and add tag to the queryKey."This chapter's vocabulary: lift state up, derived state, controlled / uncontrolled input, key, effect cleanup, dependency array, server state vs client state, optimistic update, memoization.
- What triggers a re-render? A component's own state changing, its parent re-rendering, or a consumed Context changing. A props change isn't an independent cause — it always comes from a parent re-render.
- Why do lists need keys? Why not the index? Keys let React match old and new elements when diffing; with indexes, inserting or sorting mismatches elements so local state and DOM get reused for the wrong item.
- How does useEffect's dependency array work? After each render, compare item by item with Object.is; if changed, clean up then run. An empty array = run once on mount.
- What is the virtual DOM? The object tree described by returned JSX; React diffs old and new trees to compute minimal DOM operations. The point is "declarative + efficient batched updates", not "virtual DOM is always faster than direct DOM manipulation".
Self-check
1. The parent Page updates a clock state every second; its child ExpensiveChart gets identical props every time. Without the React Compiler or memo, does Chart render every second? What structural change is better?
Yes — a parent re-render re-renders all its children. Better than memo: push the clock state down into its own Clock component so the state only affects what needs it; Chart is then no longer a child of the component that owns the clock state.
2. This code wants to clear a comment draft when linkId changes: useEffect(() => setDraft(""), [linkId]). What's wrong, and what's better?
It first renders once with the stale draft, then renders again after the effect — a flicker and an extra render. Better: in the parent, give the comment component key={linkId}. When the key changes, React treats it as a brand-new component and the state resets naturally.
3. A user quickly types "r", "re", "rea" into a search box, each firing a request from an effect. The response for "re" arrives last. What does the UI show, and how do you prevent it?
It shows results for "re", mismatching the "rea" in the box — a race. You can mark stale requests in the effect cleanup or cancel them with AbortController; better, use TanStack Query (managed by queryKey, only the current key's result is used) plus debounce.