Skip to content

Front end

Client state and data fetching

The layer that caches server data in the browser and holds UI state, using TanStack Query, SWR or Zustand depending on the build.

Most of what a web application shows is not its own state, it is a copy of the server's state that went stale the moment it arrived. Treating that copy as ordinary application state is where a great deal of front end complexity comes from. The libraries here split the two apart: a query cache owns anything fetched from the server, with its own rules for staleness, refetching, retries and deduplication, and a small store owns the handful of things that are genuinely local, such as which panel is open or who the current user is.

How we use it

ClassProfile runs TanStack Query for server data and Zustand for local state. It is a good example of why the split matters: a feed, a connections list, a follower count and a notification badge all read the same records from different screens, and one cache means one fetch and one place to invalidate when a connection request is accepted. Post visibility rules make correctness here more than cosmetic, since a cached list must not outlive a change in who is allowed to see it, which is the ground covered in four visibility levels and what they cost.

@Heartbeat uses the same pair on a public timeline with likes, bookmarks, reposts and live notification counts, and it is the build where refetch policy has a price attached: a timeline that revalidates on every window focus multiplies read volume across the whole platform, which we worked through in what a social timeline actually costs to run. Jonosakti uses SWR instead, which is a smaller library with a narrower feature set, and for a site whose homepage is a tabbed feed pulling live counts, that was enough.

What it costs you

You are running a cache, with everything that implies. Keys are strings you invented, and a mutation that invalidates the wrong key leaves a stale list on screen with no error anywhere. Nothing tells you about the query key you renamed in one file and not the other. This is the single largest source of "it works but the number is wrong" bugs in the applications above.

The defaults are opinionated and will surprise you. Refetch on focus, refetch on reconnect and background revalidation are sensible for a dashboard and wasteful for a settings page, and on metered infrastructure they turn into requests you pay for. Optimistic updates are the other trap: they are lovely when they work, and they require a correct rollback path for the case where the server says no, which is the path nobody tests.

Then there is state that ends up in two places. A user object in the query cache and a copy in the store will diverge, usually after a profile edit, and the discipline of keeping one authoritative copy has to be actively maintained.

When we would choose something else

If the data is read mostly and the page can be rendered on the server, the best client cache is the one you do not ship. Server components fetch on the server and hand down HTML, and a whole category of the problems above stops existing, which is part of the case on Next.js.

For a small application with a few screens, the platform is enough: fetch, useState and a context for the session. Reaching for a query library before you have a caching problem is how a three page app acquires a dependency it will carry for years, and plain React covers it. And where the requirement is live updates rather than cached reads, polling is the wrong tool and a socket is the right one, which is realtime and WebSockets.

Where we have used it

Every build below lists this in its stack, so the claim is checkable.