Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~24 min· topic 6 of 6

Topic 6.6

Where Each Piece of State Should Live

In one line

Keep state as close as possible to the components that use it (colocation), lift it only as far as needed to share it, and recognise the special kinds that belong elsewhere: server data in a cache, shareable UI in the URL, and form drafts in the form.

Think of it like this

A house. Your toothbrush lives in the bathroom, not in the living room; the TV remote lives in the living room, where everyone who uses it sits; the family's address lives on the gate, where visitors can read it; and your bank balance lives at the bank, not on a note on the fridge. Each thing is kept where it is used, and some things belong outside the house altogether. State is the same.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Colocation
Keeping state (and code) next to the component that uses it.
Local state
State used by one component only.
Shared state
State used by a few components, owned by their closest common parent.
Global client state
Data many distant parts of the app need, like the theme or the current user.
Server state
Data that really lives on a server. The app only holds a cached copy that can go stale.
URL state
State kept in the address bar (path or ?search=params), so it can be bookmarked and shared.
Search params
The part of a URL after ?, like ?sort=price&page=2.
Form state
The values of a form while the user is still filling it in.

Step by step

01Start local

Every new piece of state starts in the component that first needs it. Don't put it in App "in case" something else needs it later. Moving state up later is a small, safe change.

02Lift only as far as needed

When a second component needs the value, lift it to their closest common parent (Topic 6.3). If many distant components need it and drilling gets painful, that's the moment for context or a store.

03Pull out the special kinds

Before reaching for useState, ask whether the data is server data, URL data or a form draft. Each has a better home with tools built for it.

ProductsPage.jsxwhole filejsx
// URL state: shareable, survives reload, Back button works
const [params, setParams] = useSearchParams();
const sort = params.get("sort") ?? "name";

// Server state: cached, deduplicated, refetched for you (Topic 14.6)
const { data: products, isPending } = useQuery({
  queryKey: ["products"],
  queryFn: fetchProducts,
});

// Local UI state: only this component cares
const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false);

// Derived: not state at all
const sorted = products?.toSorted(byField(sort));

04The decision checklist as a picture

Run new data through these questions in order. Most of it ends up local, or not state at all.

The decision checklist as a picturediagram
Rendering diagram…

Try it yourself

  1. 1

    Fix the too-high menu

    In the colocation example, rewrite TooHigh to use the Dropdown component. Predict the console after the scripted clicks. (No too high: SlowList rendered line after the separator.)

  2. 2

    Add a URL filter

    In the URL example, add a max search param: a button "Under ₹50" that calls setSearchParams({ sort, max: "50" }), and filter with Number(searchParams.get("max") ?? Infinity). Change initialEntries to ["/products?sort=name&max=50"] to simulate opening a shared link.

  3. 3

    Classify an app's state

    For a music app, write where each lives: the current song list from the server, the playing song's id, the volume slider while dragging, the search query, the dark theme, whether the lyrics panel is open. Then check your answers against the checklist diagram.

Code & diagrams

Colocation renders less New tab

Opening the first menu renders its `SlowList` again, because the state's owner contains the list. Opening the second menu renders only `Dropdown`. Same screen, less work, and the menu code is all in one place.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

too high: SlowList rendered
colocated: SlowList rendered
--- opening both menus ---
too high: SlowList rendered
URL state: the address bar is the source of truth New tab

The page opened at `?sort=price` (as if from a shared link) and showed the cheapest first. Clicking a button changes the URL, and the list follows it. In a real app the Back button would undo the sort. React Router is taught in Phase 13.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

URL: /products?sort=price
URL: /products?sort=name
Server state belongs in a cache, not in useState (preview of Topic 14.6)jsx
// ❌ Server data copied into local state: you now own loading, errors,
//    caching, refetching and keeping other screens in sync.
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
// ...plus an effect to fetch (Phase 7) and code to refresh after edits

// ✅ A server-state library owns the cached copy.
import { useQuery } from "@tanstack/react-query";

function ProductList() {
  const { data, isPending, error } = useQuery({
    queryKey: ["products"],          // the cache key: same key, same data everywhere
    queryFn: () => fetch("/api/products").then((r) => r.json()),
  });
  if (isPending) return <p>Loading…</p>;
  if (error) return <p>Could not load products.</p>;
  return <ul>{data.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Copy URL state into useState

In the URL example, add const [sort, setSort] = useState(searchParams.get("sort") ?? "name") (remove the old sort line), and make the buttons call setSort instead of setSearchParams.

terminal
$ Preview
── what you'll see ──
/products?sort=price (the URL never changes)
The list is sorted by name after clicking By name, but the address still says sort=price.

Myth vs fact

Myth

Put all state in a global store so it's easy to find.

Fact

Global state makes every change potentially affect the whole app and hides which components depend on what. Most state is local; colocate it and keep global state small.

Myth

Data from the server is just global state.

Fact

It is a cached copy of someone else's data, with loading, errors, staleness and refetching. Tools built for server state (TanStack Query, route loaders) handle that; a plain store doesn't.

Myth

Every input needs a useState.

Fact

Many forms only need the values on submit. Uncontrolled inputs with FormData keep the draft in the DOM with no state at all.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Colocation is also a performance tool, and the cheapest one: moving state down (or passing static parts as children from above) avoids renders without memo. React's docs recommend it before reaching for memoisation (Topic 16.3).

  • ▸

    URL state needs care: everything in it is a string (parse numbers and booleans, validate unknown values), it is visible and shareable (never put secrets in it), and every change can add a history entry (React Router's setSearchParams(next, { replace: true }) avoids flooding Back for things like typing in a search box).

  • ▸

    Server state and client state mix badly in one store: you end up storing request status flags next to UI flags and hand-writing invalidation. The common modern split is a server-state library for remote data plus a small client store (or just context) for the rest (Topic 15.6).

  • ▸

    Put state that must survive a reload but shouldn't be shared (a draft, a collapsed sidebar) in localStorage and read it with useSyncExternalStore or a small hook (Topics 10.2 and 10.3). In server-rendered apps, remember storage doesn't exist on the server (Topic 20.6).

Remember this

  1. 1

    Colocation means keeping state in the lowest component that needs it. A dropdown's isOpen belongs in the dropdown, not in App. Colocated state is easier to find, delete and change, and it renders less: when state changes, React renders the owner and everything inside it, so state kept high up makes unrelated parts of the page render on every change. Start local; lift only when a second component needs it (Topic 6.3).

  2. 2

    Think in four rings. Local state: used by one component (an open menu, a hover, a text box). Shared state: used by a few nearby components, owned by their closest common parent (the selected tab, a filter). Global client state: needed all over the app (theme, current user, a shopping cart); passed with context (Phase 9) or a store such as Zustand or Redux (Phase 15). Most state is local; very little is truly global.

  3. 3

    Server state is data whose real home is a server: products, messages, a user profile. Your app only holds a cached copy that can go stale, needs loading and error states, and might be shared by many screens. Copying it into useState means writing caching, refetching and deduplication yourself. Data libraries such as TanStack Query (Topic 14.6) or a framework's loaders (Topic 13.5) manage it for you. Treat it as a separate category, not as "global state".

  4. 4

    URL state is state that belongs in the address bar: the current page, a search query, filters, sort order, the open tab of a settings page. If a user would expect to bookmark it, share it or press Back to undo it, put it in the URL (path or search params like ?sort=price). The URL is then the source of truth, and the component reads it with a router hook such as useSearchParams (Topic 13.3). Don't also copy it into useState, or the two will disagree.

  5. 5

    Form state is the in-progress values of a form before submission. Often it doesn't need React state at all: uncontrolled inputs plus FormData on submit (Topics 5.3 and 5.4) keep the draft in the DOM. Use controlled state when the UI reacts as you type (live validation, a character counter). For big forms, a form library keeps it for you (Topic 5.7). Drafts that must survive a reload can go in localStorage (Topic 10.2).

  6. 6

    The decision checklist, for every new piece of data: 1) Can it be calculated from something else? Then it isn't state (Topic 6.2). 2) Does it come from a server? Server-state cache. 3) Should it survive a reload or be shareable via a link? URL (or storage). 4) Is it the draft of a form? Form state, often uncontrolled. 5) Otherwise it is UI state: put it in the lowest component that uses it, lift it to the closest common parent if several need it, and reach for context or a store only when many distant components need it.

Explain it without notes

01

What is state colocation and why is it a good default?

02

Name the kinds of state in a typical app and where each usually lives.

03

When should state live in the URL? What should you watch out for?

04

Why is server data treated differently from other state?

Practice

01

An App component holds const [isSearchOpen, setIsSearchOpen] = useState(false) and passes it down three levels to a SearchButton, which is the only component that uses it. Where should it live? Rewrite SearchButton.

02

Make a paginated list whose page number lives in the URL (?page=2) using useSearchParams, with Previous and Next buttons.

Trade-offs

  • ↔

    Colocated state is simple and fast but must be lifted when requirements change; global state is reachable everywhere but couples distant parts of the app and renders more.

  • ↔

    URL state is shareable and survives reloads but is public, stringly typed and adds history entries; keep only what a user would want in a link there.

Done when you can

  • I start every piece of state in the lowest component that needs it.

  • I can name local, shared, global, server, URL and form state and their usual homes.

  • I put bookmarkable UI state in the URL and read it with a router hook.

  • I know server data belongs in a cache built for it.

  • I can walk new data through the decision checklist.