Command Palette

Search for a command to run...

Hectal
PHASE 15Advanced ~26 min· topic 1 of 6

Topic 15.1

Kinds of State and Where Each Belongs

In one line

Not all state is the same: UI state, form state, server state, URL state and global client state each change for different reasons and each has a best home, so picking the right tool starts with naming the kind.

Think of it like this

A kitchen. Milk goes in the fridge, rice in a jar, knives in a drawer, and the shopping list on the door where everyone sees it. Nobody keeps everything in one giant box, because each thing has its own needs: cold, dry, safe, visible. App state is the same. A dropdown being open, the text in a form, the user's orders from the server, the current page number in the address bar and the theme are different kinds of data, and each one has a natural place to live.

Words you'll meet

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

UI state
How the screen looks right now: open menus, selected tabs, hover. Fine to lose on reload.
Form state
What the user is typing in a form, which fields were touched, and the validation errors.
Server state
Data whose real owner is a server. Your app keeps only a cached copy that can go stale.
URL state
Data kept in the address bar (path and ?query), so it survives reloads and can be shared as a link.
Global client state
Data made in the browser that many far-apart components need, like the theme or a cart.
Derived state
A value you can compute from other state, like a total. Compute it during render instead of storing it.
Single source of truth
Each fact is stored in exactly one place, and everything else reads from there.
Cache
A saved copy of earlier answers, kept so you don't have to ask again. It can become out of date.

Step by step

01List every piece of data on one screen

Take a shop page. Write down everything that changes: the product list, the sort order, whether the filter panel is open, the search box text, the cart, the theme, the logged-in user.

Don't think about tools yet. Only list the facts.

02Ask who owns the truth

The product list and the user's profile are owned by the server. The sort order and search are owned by the URL (you want them in a shared link). The panel and the text being typed are owned by this screen. The cart and the theme are owned by the browser session and read everywhere.

Ask who owns the truthdiagram
Rendering diagram…

03Remove what you can compute

The sorted list, the cart total and the item count are derived: compute them from the real state on every render. Storing them would create a second copy that can disagree with the first.

Cart.jsxwhole filejsx
// Store only the items. Work out the rest during render.
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
const count = items.length;

04Give each kind its home

Server state goes to a query library, URL state to the router, form and UI state stay local, and only the small remainder (cart, theme, session) becomes global client state. The live examples below show two of these homes at work: the URL keeping a sort order through a reload, and a query cache sharing one request between two components.

ShopPage.jsxwhole filejsx
function ShopPage() {
  const [params, setParams] = useSearchParams();        // URL state
  const [panelOpen, setPanelOpen] = useState(false);     // UI state
  const { data: products } = useQuery({                  // server state
    queryKey: ["products", params.get("sort")],
    queryFn: () => fetchProducts(params.get("sort")),
  });
  const cartCount = useCart((s) => s.items.length);     // global client state
  // ...
}

05Check the result with three questions

Is each fact stored once? Does a reload keep what the user expects (the filter) and drop what they don't care about (the open panel)? Does changing one fact re-render only the parts that show it? If all three answers are yes, the design is right, whatever tools you used.

Try it yourself

  1. 1

    Move the panel into the URL

    In the first example, predict what happens if the filter panel's open flag is stored in the URL as ?panel=open instead of useState. Change the Filters button to setParams({ sort, panel: panelOpen ? "" : "open" }) and read panelOpen from params. Run it: the panel now survives the reload.

    Is that what users want? For a filter panel, usually not. For an open product dialog (?product=42), often yes, because people share that link.

  2. 2

    Remove the cache sharing

    In the second example, give PlanBadge its own key: queryKey: ["user-plan"]. Predict how many network request lines appear. Run it: two requests, because two cache entries.

  3. 3

    Find the derived state

    Look at Shop: is list stored anywhere? It is computed on every render from PRODUCTS and sort. Try storing it in useState instead and notice that clicking Sort by price no longer re-sorts unless you also update the copy.

Code & diagrams

URL state survives a reload, UI state doesn't New tab

The script sorts by price, opens the filter panel, then "reloads" (a new `key` remounts the router with the last URL). The sort comes back because it lived in the URL. The panel is closed again because it was UI state, and losing it is fine.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

URL: /shop | panel open: false
URL: /shop?sort=price | panel open: false
URL: /shop?sort=price | panel open: true
URL: /shop?sort=price | panel open: false
Server state is a shared cache, not component state New tab

Two components need the user, but there is **one** network request: both read the same cache entry keyed `["user"]`. With `useState` + `useEffect` in each component you'd get two requests and two copies that can disagree.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Header: loading
PlanBadge: loading
network request #1
Header: Asha
PlanBadge: Pro
Each kind of state and its homediagram
Rendering diagram…

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 server data into global state

Fetch the user once in App, store it in a global store, and let the profile page update the server with a PATCH request without updating the store.

terminal
$ Preview
── what you'll see ──
Profile page: Name saved as "Asha K."
Header: Hi, Asha

Break #2

Keep the search filter in useState

Store the search text and the page number in useState on a results page, then press the browser's Back button after opening a result.

terminal
$ Preview
── what you'll see ──
Results page shows page 1 with an empty search box

Myth vs fact

Myth

Big apps need one global store for all state.

Fact

Most state is local, server or URL state. A global store is for the small part that is made in the browser and needed everywhere. Putting everything in it makes more components re-render and hides who owns what.

Myth

Data fetched from an API is just state, so useState is fine for it.

Fact

It's a cached copy of someone else's data. It needs loading and error states, sharing, refetching and invalidation, which is exactly what server-state libraries add.

Myth

URL state is only for the page path.

Fact

Search text, filters, sort order, the selected tab and the open item are all good URL state: anything a user would bookmark, share, or expect Back to restore.

Pro corner

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

  • ▸

    The move from "one Redux store for everything" (common around 2016 to 2019) to "server cache plus small client store" happened because most of what those stores held was server data with hand-written loading flags and cache invalidation. RTK Query and TanStack Query exist to delete that code.

  • ▸

    URL state has limits: keep it small (browsers and servers cap URL length, a few thousand characters is a safe budget), don't put secrets in it (URLs end up in logs, history and Referer headers), and validate it, because users can edit it by hand.

  • ▸

    Some state has two owners at different times: a form edits a copy of server data. Model it explicitly: the server copy lives in the query cache, the draft lives in form state initialised from it (with a key to reset when the item changes, Topic 6.6), and saving writes back and invalidates.

  • ▸

    Persisted client state (localStorage) is a cache too: it can be stale, from an older app version, or edited by the user. Version it and migrate it (Zustand's persist has version and migrate, Topic 15.3).

Remember this

  1. 1

    State is any data that can change while the app runs and that affects what the screen shows. The big mistake in growing apps is treating all of it as one thing and putting it all in one place (often one giant context or one Redux store). Ask two questions about each piece of data: who owns the truth? (this component, the browser's address bar, or a server far away) and who needs to read it? (one component, a few neighbours, or the whole app). The answers point to its home.

  2. 2

    UI state is about how the interface looks right now: a menu is open, a tab is selected, a row is hovered, a modal is showing. It is owned by the screen, usually needed by one component or a small group, and it's fine to lose it on reload. Home: useState in the component, or lifted to the nearest common parent (Topic 6.4). Form state is the values being typed, which fields were touched, and validation errors. Home: useState/useReducer in the form, uncontrolled inputs with FormData (Phase 5), React 19 form actions (Phase 19), or a form library such as React Hook Form for very large forms.

  3. 3

    Server state is data whose real owner is a server: the product list, the user's profile, the comments. Your app holds only a copy, a cache that can go out of date when someone else changes the server. It needs things normal state doesn't: loading and error flags, sharing one request between components, refetching when stale, retrying, and invalidating after a change. Home: a server-state library such as TanStack Query (Topic 14.6), SWR or RTK Query (Topic 15.4), or a framework's loaders (Phase 13, Phase 20). Copying server data into useState or a global store is the most common state bug in real apps: two copies drift apart.

  4. 4

    URL state is anything that identifies what the user is looking at: the page, the item id, search text, filters, sort order, the page number. Its owner is the address bar, so it survives reloads, works with the Back button, and can be shared as a link. Home: the router (useParams, useSearchParams, Topic 13.4). If a person would want to bookmark it or send it to a friend, it belongs in the URL, not in useState.

  5. 5

    Global client state is data created in the browser that many distant parts of the app need: the theme, the logged-in user's session details, a shopping cart before checkout, feature flags, an unsaved draft. This is the only kind that truly needs app-wide tools: context (Phase 9) when it changes rarely, or an external store such as Zustand or Redux when it changes often or is read in many places (Topics 15.2 to 15.4). Once server and URL state move to their own homes, most apps discover that very little global client state is left.

  6. 6

    Two rules apply to every kind. Don't store what you can compute: a total, a filtered list, or isValid is derived state, worked out during render from the real state (Topic 6.2). And keep state as close as possible to where it's used: lifting it higher than needed makes more components re-render and makes the code harder to follow. A good state design is mostly about putting each fact in exactly one place.

Explain it without notes

01

Name the main kinds of state in a React app and the best home for each.

02

Why is copying server data into a global store or useState a problem?

03

How do you decide whether a value belongs in the URL?

04

What is derived state and why shouldn't you store it?

Practice

01

Classify each piece of data on a blog's post page: the post body, the comment being typed, whether the share menu is open, the current post's slug, the dark-mode setting, the number of comments.

02

A list page keeps const [page, setPage] = useState(1). Move the page number into the URL so reloads keep it.

Trade-offs

  • ↔

    URL state is shareable and survives reloads, but every change adds a history entry (use setParams(next, { replace: true }) for rapid changes such as typing) and values are strings you must parse and validate.

  • ↔

    Splitting state across many homes (query cache, router, local state, a store) means each piece is simple, but a newcomer must learn where each kind lives. Write the convention down once for the team.

Done when you can

  • I can name the five kinds of state and give an example of each.

  • I can say who owns the truth for any piece of data on a screen.

  • I keep server data in a query cache instead of copying it into state.

  • I put ids, filters, sort and page numbers in the URL.

  • I compute derived values during render instead of storing them.