The problem
Server state is not app state.
A selected tab or a half-typed form is yours: it stays where you left it. The orders on a server are not. Someone else can change them, your copy ages while the user reads it, three screens want it at once, and the network drops at the worst moment.
A FutureBuilder gets the first load on screen. What comes after is the real work — caching, sharing one request between readers, refreshing when the app comes back, retrying, cancelling, invalidating after a write, paging. query_kit does that work the way TanStack Query does it on the web, so a screen only says what it shows.
Try it
A write on screen before the server answers.
The showcase's optimistic-updates screen, running in this page against an in-memory backend. Add a todo and it is in the list at once. Then switch on Refuse next write, choose Via cache and add another: the row appears, the server refuses it, and the cache rolls back.
What you get
The work after the first load, done by the cache.
- Caching and deduplication
- Five widgets reading one key make one request and share its answer.
- Stale-while-revalidate
- A second visit renders from the cache at once, and refetches behind it when the data is older than its
staleTime. - Refetch on focus and reconnect
- The app returning to the foreground, or the network returning, refreshes what is on screen.
- Mutations and invalidation
- A write names the keys it made stale, and whatever shows them fetches again.
- Optimistic updates
- Show the write before the server answers, and roll it back when the server refuses.
- Infinite and paginated lists
- Pages fetched in either direction, kept under one key, refetched in order.
- Retries and cancellation
- Failed fetches retry with backoff; a fetch whose function reads
context.signalis cancelled when its last reader leaves. - Offline-aware
- Told when the device is offline, queries wait for the network instead of failing, and writes made offline pause until it is back.
- Four equal ways to read
context.query,QueryBuilder,QueryMixinor aValueListenable. None of them is the default.- Sealed results
- A
switchoverQueryPending,QueryErrorandQuerySuccessis exhaustive, so there is nodata!. - A pure-Dart core
- The cache runs without Flutter: in a CLI, on a server, in a shared package.
- Nothing but Flutter
- No hooks, signals or connectivity package required. Connectivity is a
Stream<bool>you bring yourself.
Ported, not inspired by
Upstream's own tests say it behaves the same.
TanStack Query's test suite is ported alongside the code, under upstream's own test names, so the two files read side by side. Every case that is not ported is listed with its reason, and where the Dart port differs on purpose, the differences are written down as behaviour.
Credits
Thank you to Tanner Linsley and to everyone who has built and maintained TanStack Query. Every good idea here is theirs, published under their MIT licence, whose notice each package carries in LICENSE-TANSTACK.
It is not theirs. Not affiliated with, endorsed by, reviewed by, or connected in any way to Tanner Linsley, the TanStack team, or the TanStack organisation. Please do not take problems with this package to them — they belong here.