TanStack, or logic without appearance
TanStack is a set of libraries built around one principle: you get the logic and write the appearance yourself. A table imposes no styles, a form renders no fields, a router holds no opinion about how a page looks.
That philosophy solves a problem that surfaces sooner or later with ready components. A library carrying its own appearance is convenient until a project needs something its author did not anticipate, and then the fight with styles begins.
The set today counts eighteen public libraries, listed on the project site across five groups, and that is the first thing to state plainly: their maturity differs drastically. Treating the name as one thing leads to putting something at an early stage of development into production.
What is stable and what is not
The split as of mid 2026 looks like this and deserves knowing before deciding.
Stable and production ready are: the server data layer, the router, the table, forms, and long list virtualisation. Those are projects with years of history, an enormous deployment base, and a predictable release cycle.
The full stack framework reached release candidate status for its first version. Its interface is considered stable and its features complete, and products built on it already run, while the pre version one label still applies.
The locally running data layer and the tooling for language models are in beta. The state store is in alpha.
The rest of the list arrived over recent months and carries earlier labels still. The library for debouncing and rate limiting calls is in beta, while keyboard shortcuts, the document model for content, and syntax highlighting are in alpha. Four entries on the list are not application libraries at all but project tooling: a devtools panel, shared configuration for published packages, a project scaffolder, and a mechanism for shipping agent instructions inside npm packages.
The practical conclusion is simple. The first group suits everywhere. The framework can be considered on a new project, provided you accept that version one has not landed. Treat the rest as early software, meaning use it in side projects rather than in a system meant to run for years.
The server data layer
This is the oldest and most used element of the set, and simultaneously the one that changed how applications are written the most.
It solves a problem nobody named plainly for years: data coming from a server is a different thing from application state. It requires fetching, caching, refreshing, retrying on network errors, and invalidating after changes.
const { data, isPending, error } = useQuery({
queryKey: ['articles', category],
queryFn: () => fetchArticles(category),
staleTime: 5 * 60 * 1000,
})The query key is the central concept here and the most often misused. It states the data's identity, so it must contain everything affecting the result. Omitting the category in the example above means that after changing category you see the old data, since to the library it is still the same query.
On a larger project it pays to gather the keys in one place rather than typing strings across a dozen components.
export const keys = {
articles: {
all: ['articles'] as const,
list: (category: string) => ['articles', 'list', category] as const,
one: (id: string) => ['articles', 'one', id] as const
}
}The gain is double. A typo in a key becomes impossible, since the compiler checks the function call, and invalidating a whole branch comes down to supplying the shared prefix.
The second concept worth understanding is staleness time. By default data is considered outdated immediately, so the library refreshes it on returning to the tab and on remounting a component. That is sometimes desirable and sometimes excessive, and setting it deliberately reduces server traffic more than any other change.
The third is invalidation after writes. Once a change is sent you must tell the library which data stopped being current, otherwise the user sees the state from before their own edit.
const client = useQueryClient()
const save = useMutation({
mutationFn: saveArticle,
onSuccess: () => {
client.invalidateQueries({ queryKey: ['articles'] })
}
})Supplying only the start of a key invalidates every query beginning with it, so one call covers the list across all categories at once. That behaviour is worth remembering, since it lets you build keys hierarchically and invalidate whole branches with a single call.
The router and the framework
The router is the second project worth knowing, mainly for one characteristic.
Routes are fully typed, path parameters and query parameters included. That means a link to a non existent route or a missing parameter is a compile error rather than a blank page discovered by a user.
Route level data loading joins that, tied into the data management layer. Data starts fetching when navigation begins rather than after a component renders, which removes the characteristic loading state flicker.
The full stack framework sits on that router and adds server rendering plus server functions. The difference from Next.js comes down to approach: there the server is the starting point and interactivity is added as islands; here the client application is the starting point and server rendering is added to it.
That difference matters when choosing. A highly interactive application, a dashboard, or a tool behind a login suits the second approach better. A content led site meant to be indexed suits the first.
Tables and forms
The table best demonstrates the point of the no appearance approach.
You get sorting, filtering, pagination, grouping, column resizing, and row selection. You get not one HTML tag, so you render it yourself, exactly as the rest of your interface looks.
For a table of business data that is the right answer, since requirements are always unusual: an actions column, an expandable row, a cell holding a chart. A ready table handles three of those and on the fourth the fight begins.
Forms follow the same path and solve a problem that is real on larger forms: recomputing the whole form on every keystroke. The library tracks fields separately, so a change in one does not render the other thirty.
Note, though, that the no appearance approach carries a price. The first table takes a day rather than an hour, since you write everything yourself. The return appears on the third, when it turns out each looks different and none required working around ready made styles.
Virtualising long lists
This is the least known element of the set, and it solves a problem large data sets leave no other way around.
The browser renders every row you put into the document. At five hundred rows nobody notices; at fifty thousand the tab stops responding and scrolling itself starts dropping frames. Virtualisation means rendering only the elements that actually fit the visible area, plus a small buffer above and below it.
The library computes element positions and tells you which ones to draw and where to place them. Again you get no markup, so you virtualise any structure: a list, a grid of tiles, a table, or a timeline.
Two things are worth knowing before adoption. Fixed height elements are the simple case, since each position follows from multiplication. Variable height elements require measuring after render, which the library handles, at the cost of some complexity and a possible scrollbar jump during fast jumping.
The second is the effect on accessibility and in browser search. Elements outside the view do not exist in the document, so the find shortcut will not locate them and a screen reader will not announce them. On a results list that is usually acceptable; on a long text document it is not, and pagination is the better answer there.
Writes and optimistic updates
Reading is half the work with a server, and the other half causes more trouble, since it concerns state that can fail.
The basic pattern is simple: send the change, wait for the response, invalidate the data that change touched. The interface then shows a loading state for the duration of the round trip, which is invisible on a fast connection and irritating on a slow one.
An optimistic update reverses the order. You change the data in the cache immediately, send the request in the background, and on error roll the change back to the previous state. The user sees an instant reaction, which on toggles, likes, or marking tasks done is the difference between a live interface and a sluggish one.
The price is that you must be able to return to the previous state. So before the change you save the previous value, and in the error handler you restore it.
const toggle = useMutation({
mutationFn: toggleDone,
onMutate: async (id) => {
await client.cancelQueries({ queryKey: ['tasks'] })
const previous = client.getQueryData(['tasks'])
client.setQueryData(['tasks'], (old: Task[]) =>
old.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
)
return { previous }
},
onError: (_error, _id, context) => {
client.setQueryData(['tasks'], context?.previous)
},
onSettled: () => {
client.invalidateQueries({ queryKey: ['tasks'] })
}
})Cancelling queries on the first line is the most commonly skipped part, and it solves a real problem: a response from a request issued before your change could arrive later and overwrite it with stale data. Skipping that step gives an interface that after a failed write still shows a change which never reached the server, and that is worse than having no optimistic update at all.
It also pays to separate two cases. For a reversible, low stakes action such as collapsing a section, an optimistic update is right. For an irreversible or costly operation such as submitting a payment, better to show a pending state and wait for server confirmation, since rolling such a change back in the interface rolls it back nowhere else.
TanStack against the alternatives
| Option | Strength | Weakness | Pick it when |
|---|---|---|---|
| TanStack | Logic without appearance, typing, a mature data layer | More work up front, uneven project maturity | An application with its own visual system |
| Ready components | Appearance included, a fast start | Fighting styles on unusual requirements | An internal dashboard, a prototype |
| Zustand | A simple client state store | Does not handle server data | State shared across the application |
| Framework mechanisms | No extra dependencies | Fewer options with complex caching | An application leaning heavily on server rendering |
The last row deserves honest consideration, since with server rendered applications some problems this layer solves simply do not arise. Data fetched on the server and passed to a component needs no client side caching.
The server data layer and a client state store do not compete but cover different things. A typical application uses both: the first for backend data, the second for state existing only in the browser.
How to choose from the set
Practical guidance, since the temptation to take everything at once is real and usually a bad idea.
Start with the server data layer. It gives the largest gain at the smallest adoption cost and slots into an existing project without changing anything else.
Add the table when you have a specific table to build and know a ready one will not suffice. Learning it in advance is not worthwhile.
Consider forms on forms holding a dozen or more fields. On a three field form ordinary component state is simpler and sufficient.
The router and the framework are an architectural decision made at a project's start and not changed later without a rewrite. Settle it deliberately rather than because the rest of the set worked out.
Do not put early stage projects into production systems. That is not a judgement on their quality but simple arithmetic: alpha software changes its interface, and you pay each time with a rewrite.
Common mistakes
The first is an incomplete query key. Omitting a parameter affecting the result means that after a change you see the old data with no error at all.
The second is leaving the default staleness time. Data refreshed on every return to the tab generates traffic you usually do not need.
The third is no invalidation after writes. The user then sees the state from before their own edit, which looks like a lost change.
The fourth is keeping server data in a client state store. That produces a manual layer reimplementing caching and refreshing, meaning what the data layer does itself and better.
The fifth is treating the whole set as equally mature. Maturity stretches from projects with years of history to alpha.
The sixth is reaching for the table without your own visual system. The no appearance approach pays off when you have something to match.
The seventh is virtualising lists that do not need it. At two hundred rows the gain is unmeasurable, and the cost is losing in browser search plus another layer of code to maintain. Measure whether scrolling actually drops frames first, and only then reach for it.
The eighth is an optimistic update with no saved previous state. After a failed write the interface then shows a change that never reached the server, and the user finds out only after reloading the page.
FAQ
Which libraries in the set are stable?
The server data layer, the router, the table, forms, and list virtualisation. The full stack framework is a release candidate for version one, the local data layer and language model tooling are in beta, and the state store in alpha. The remaining entries, added most recently, carry a beta or alpha label.
How does the data layer differ from a state store?
In purpose. The first handles data coming from a server, caching, refreshing, and retrying included. The second holds state existing only in the browser, an open menu for instance. A typical application uses both for different things.
Why do I see old data after changing a parameter?
Most often because the query key does not include that parameter. The key states the data's identity, so it must cover everything affecting the result, otherwise the library treats it as still the same query.
Will the full stack framework replace Next.js?
For highly interactive applications it is sometimes a better fit, since it starts from a client application and adds server rendering. For content led sites Next.js has a more mature ecosystem and a longer deployment history.
Is taking the whole set at once worthwhile?
No. Start with the server data layer, since it gives the largest gain at the smallest cost. Add the rest when you have a specific problem to solve, and keep beta and alpha projects out of production systems.
Documentation sits on the project site, and release news on the team blog.