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.
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.
// 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.
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
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=openinstead ofuseState. Change the Filters button tosetParams({ sort, panel: panelOpen ? "" : "open" })and readpanelOpenfromparams. 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
Remove the cache sharing
In the second example, give
PlanBadgeits own key:queryKey: ["user-plan"]. Predict how manynetwork requestlines appear. Run it: two requests, because two cache entries. - 3
Find the derived state
Look at
Shop: isliststored anywhere? It is computed on every render fromPRODUCTSandsort. Try storing it inuseStateinstead and notice that clicking Sort by price no longer re-sorts unless you also update the copy.
Code & diagrams
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
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: falseTwo 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
Console
Nothing logged yet.Expected console output
Header: loading
PlanBadge: loading
network request #1
Header: Asha
PlanBadge: ProBreak 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.
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.
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
Refererheaders), 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
keyto 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
persisthasversionandmigrate, Topic 15.3).
Remember this
- 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
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:
useStatein 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/useReducerin the form, uncontrolled inputs withFormData(Phase 5), React 19 form actions (Phase 19), or a form library such as React Hook Form for very large forms. - 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
useStateor a global store is the most common state bug in real apps: two copies drift apart. - 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 inuseState. - 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
Two rules apply to every kind. Don't store what you can compute: a total, a filtered list, or
isValidis 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
Name the main kinds of state in a React app and the best home for each.
Why is copying server data into a global store or useState a problem?
How do you decide whether a value belongs in the URL?
What is derived state and why shouldn't you store it?
Practice
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.
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.