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.

⏱ ~70 minDeep (rendering model, state, effects) Use (hooks, forms, TanStack Query)
By the end of this chapter you should be able to:
  • 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 useEffect is 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

Opening question

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)

State data React holds Component fn runs again (render) must be pure New JSX tree a "description" of UI diffed against the last DOM only diffs applied User clicks → event handler calls setState(newValue) → next round You own two things: ① a pure function from state to UI ② updating state in events "Diff and patch the DOM" is entirely React's job
React's update loop. In React terms, the middle steps are render and reconciliation; the last is commit.
Analogy: a template engine that re-runs itself

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

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:

  1. Re-rendering starts where the state lives. Its parents and siblings are unaffected.
  2. 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.
  3. memo lets 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-written useMemo, useCallback or memo.

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.

Effects run twice in development

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 toDon'tDo
Compute a value from props/stateEffect + setStateCompute during render
Respond to a user action (submit, click)Set a flag and have an effect watch itDo it in the event handler
Fetch data from the serverHand-written effect + fetchFramework loaders / Server Components / TanStack Query
Reset state when a prop changessetState in an effectGive 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 dataWhere it livesLinkNote example
Used by one component onlyLocal useStateWhether a dropdown is open
Shared by a few neighborsLifted to the common parentSearch query and the result list
Should survive refresh and be shareableURL query paramsCurrent tag filter, page number
Comes from the serverServer cache (TanStack Query / framework loaders)Bookmark list, user profile
Read deep in the tree, rarely changesContextTheme, current user
Complex, cross-page, purely client-sideA store such as ZustandOffline drafts, complex editor state
A rule of thumb

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.

Where this lands in LinkNote

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).

Tell the agent
Vague
Precise
"Add tag filtering to the list page."
"Keep the tag filter in the URL query param 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."
"The page keeps re-requesting, fix it."
"LinkList's useEffect depends on an options object recreated every render, causing a request loop. Switch the fetching to TanStack Query, delete the effect, and explain under which conditions the request fires after the change."

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.

Interview corner
  • 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.