Frontend · Guide

Routing & State

Client-side routing, server state and caching, global stores, and forms — where data lives once an app outgrows one component tree.

— min read Frontend

Not All State Is the Same

Most state-management pain comes from treating server data like local data. They have different problems, so they want different tools.

Sort every value in your app into one of four buckets before choosing a library. The bucket decides the tool, and the wrong bucket is why so many applications end up with a thousand-line store nobody understands.

KindExamplesBelongs in
LocalIs this dropdown openuseState, in the component
SharedTheme, signed-in user, localeContext, or a small store
ServerAnything fetched from an APIA caching data layer
URLCurrent page, filters, search termsThe URL itself

The last one is the most commonly missed. If a value should survive a refresh or be shareable as a link — the current tab, a filter, a page number — it belongs in the URL, not in a store.

Client-Side Routing

A router maps the URL to a component and swaps it without a full page load. It keeps the address bar honest, so back, forward, refresh and sharing all work as users expect.

The URL is a state store you get for free — and the only one that survives a refresh, can be bookmarked, and can be pasted to a colleague.

PartHoldsExample
PathWhat you are looking at/orders/42
Route paramsWhich record42
Query stringFilters, sort, page, search?status=open&page=2
HashA position within the page#delivery
A router that swaps content without moving focus leaves screen-reader and keyboard users where they were — often at the bottom of the previous page. On navigation, move focus to the new heading and announce the change.

Two things clients frequently get wrong: scroll position, which should reset on a new page but be restored on back; and the server config, which must serve index.html for unknown paths or a deep link refreshed gives a 404.

Server State & Caching

Data from an API is not really your state — it is a cached copy of state that lives somewhere else and can change without telling you. That framing is what the dedicated libraries are built around.

Hand-rolling it means writing the same four pieces every time: loading, error, the data, and the refetch. Then the harder ones — deduplicating concurrent requests, invalidating after a mutation, not showing a spinner for a cached value you already have.

ProblemWhat a data layer gives you
Two components request the same thingOne request, shared result
Data goes staleBackground refetch on focus or interval
Spinner flashing on every visitServe the cache, revalidate behind it
A mutation changes a listInvalidate the affected keys
Slow, obviously-correct updatesOptimistic update with rollback on failure
Copying fetched data into a global store is the classic mistake. You now maintain two copies of something the server owns, and they drift. Let the cache be the source of truth and read from it.

Every request needs a stable key — usually the endpoint plus its parameters. It is what deduplication, caching and invalidation all key off, and getting it wrong produces the confusing bugs.

Global State Stores

Once server data and URL state are handled properly, genuinely global client state is a much smaller problem than it first appears — often a theme, a signed-in user, and the contents of a cart.

Reach forWhenCost
Lifting state upTwo or three nearby componentsNone — do this first
ContextGlobal, rarely changesEvery consumer re-renders on change
A small storeShared, changes oftenA dependency, subscribe-by-slice
A full flux storeComplex flows needing an audit trailSignificant boilerplate

Whatever you choose, keep the store normalised and minimal: store ids, derive the rest. Denormalised copies of the same entity in three places is how a store becomes unmaintainable.

The useful question is not "which library" but "what actually needs to be global". Most teams that answer that honestly find they need far less machinery than they were about to install.

Forms & Validation

Forms are the densest state in most applications: a value, whether it has been touched, whether it is valid, why not, and whether the whole thing is submitting.

Start from the platform. Native required, type and pattern handle a surprising amount, work before JavaScript loads, and are announced correctly for free.

DecisionSensible default
When to validateOn blur, then on every change once it has errored
Where errors appearBeside the field, tied with aria-describedby
Marking invalidaria-invalid, plus text — never colour alone
On submit failureMove focus to the first invalid field
While submittingDisable the button; keep values on failure
Validating on every keystroke from the first character tells someone their email is invalid while they are still typing the second letter. Wait for blur before the first complaint.

Client validation is a courtesy, never a control. The server must validate everything regardless, because anything can post to your endpoint.

Interview Questions

How do you decide where a piece of state belongs?

Sort it: local to one component, shared across a few, owned by the server, or belonging in the URL. Anything that should survive a refresh or be shareable as a link goes in the URL. Anything fetched belongs in a cache, not a store.

Why treat server data differently from client state?

It is a cached copy of something you do not own and that can change without telling you. That brings problems client state does not have — staleness, deduplication, invalidation after mutations — which is exactly what a data layer solves.

What breaks when a single-page app is deployed without server config?

Deep links. The client router handles in-app navigation, but refreshing /orders/42 asks the server for a file that does not exist. The server must serve index.html for unknown paths.

What is an optimistic update?

Applying the expected result immediately, before the server confirms, then rolling back if it fails. It makes an interface feel instant, and it needs the rollback path to actually work.

What does client routing break for accessibility?

Focus and announcement. The URL changes and content swaps, but focus stays where it was and nothing is announced. Move focus to the new page heading on navigation.

Context or a store?

Context for global values that rarely change — theme, user, locale — since every consumer re-renders when the value changes. A store with per-slice subscriptions for shared state that changes often.

Quick Quiz

1. A filter that should survive a refresh belongs in…
2. Copying fetched API data into a global store causes…
3. Refreshing a deep link 404s. The fix is…
4. First validation message should normally appear…
5. After client-side navigation you should move focus to…