Coming from TanStack Query (JS)
The behaviour is TanStack Query's — the tests say so — but the surface is Dart's. This page maps the JavaScript names to the ones here. Where the port deliberately behaves differently, differences from TanStack Query says how. It is a community port, not TanStack's own; credits, and what this is not says what that means.
Two rules shape most of the table:
nullmeans "not configured" on every option field, so an option with a real "off" value is a sealed value type (StaleTime,GcTime,Enabled,RetryPolicy,RetryDelay,RefetchOn,RefetchInterval), never a magic number, string or boolean.- The result is a sealed type —
QueryPending,QuerySuccess,QueryError— so aswitchis exhaustive and there is nodata!. AQueryErrorstill carriesstaleData.
Reading a query
useQuery has no single counterpart in the binding. It offers four
equal alternatives — there is no recommended default, pick per situation.
The hook names themselves are in an optional add-on,
query_kit_hooks, on flutter_hooks: one more way to
read the same cache, for widgets that are already HookWidgets, and not a
replacement for the four. Its column says what to call there.
| JS | The binding | With query_kit_hooks |
|---|---|---|
useQuery(options) in a component | QueryBuilder<T>(options: …, builder: …) | useQuery(options) in a HookWidget; useSelectQuery(options) when the options carry a select |
QueryController<TQueryData, TData>(client, options) — a ValueListenable | ||
with QueryMixin on a State, then watchQuery(options) in build | ||
context.query(options) in any widget under a QueryClientProvider | ||
useInfiniteQuery | InfiniteQueryBuilder / InfiniteQueryController / watchInfiniteQuery / context.infiniteQuery | useInfiniteQuery(options), which returns the InfiniteQueryController |
useMutation | MutationBuilder / MutationController / watchMutation / context.mutation | useMutation(options), which returns the MutationController |
useQueries({ queries }) | QueriesObserver / QueriesBuilder, homogeneous: one data type per collection, select when the selected type differs. For different types, (a, b).combine(…) over a record of results | useQueries(queries), one data type per list as well |
useQueries({ queries, combine }) | combine over the list of results, see combining queries | useCombinedQueries(queries, combiner, keys: […]) |
useMutationState({ filters, select }) | MutationStateObserver / MutationStateController | useMutationState(filters: …, select: …); useTypedMutationState over the mutations of one type |
useIsFetching(filters) | IsFetchingController(client, filters: …), a ValueListenable<int>; the snapshot is client.isFetching() | useIsFetching(filters: …) |
useIsMutating(filters) | a MutationStateController filtered on MutationStatus.pending, read for its length; the snapshot is client.isMutating() | useIsMutating(filters: …) |
usePrefetchQuery, usePrefetchInfiniteQuery | client.query(options).ignore(), as under the client | usePrefetchQuery(options), usePrefetchInfiniteQuery(options) |
useQueryClient() outside a build (a handler) | QueryClientProvider.read(context) — the same client, without subscribing | the same |
useQueryClient() | QueryClientProvider.of(context) | useQueryClient() |
QueryClientProvider | QueryClientProvider(client: …, child: …) | the same |
new QueryObserver(client, options) | client.observe(options) or QueryObserver(client, options) | the same |
The client
| JS | Here |
|---|---|
fetchQuery, prefetchQuery, ensureQueryData | one QueryClient.query(options); prefetch is client.query(…).ignore(), "only if nothing is cached" is staleTime: StaleTime.static |
ensureQueryData({ revalidateIfStale: true }) | client.query(options, revalidateIfStale: true) — cached data now, refresh behind it |
getQueryData<InfiniteData<…>>(key) | getInfiniteQueryData<TPage, TParam>(key) |
fetchQuery({ select }) | none — await the future and map it |
setQueryData(key, value) | setQueryData(key, value) |
setQueryData(key, updaterFn) | updateQueryData(key, (previous) => …); returning null leaves the cache untouched |
setQueriesData(filters, updaterFn) | updateQueriesData(updater, filters: QueryFilters(…)) — every filter parameter in the core is a named filters: |
getQueryData(key) | getQueryData<T>(key) — a type mismatch throws QueryDataTypeError |
a key read as a looser type (number | undefined where number was cached) | throws too: one key, one exact type — int and int?, List<Task> and List<Object?> are different types, and getQueriesData<T> checks like getQueryData<T> |
invalidateQueries({ queryKey }) | invalidateQueries(filters: QueryFilters(queryKey: …)) |
queryKeyHashFn | gone: QueryKey is a value type with structural equality; debugString is the readable form |
focusManager, onlineManager, notifyManager (module-level) | per-client instances: client.focusManager, client.onlineManager, client.notifyManager |
setMutationDefaults(key, { onSuccess, … }) | MutationDefaults carries mutationFn, retry, retryDelay, networkMode, gcTime, scope, meta — no callbacks |
Options
| JS | Here |
|---|---|
queryKey: ['tasks', id] | QueryKey(['tasks', id]) |
queryFn: ({ signal }) => … | queryFn: (context) => …; context.signal is a QueryCancelToken, and signal.onCancel(…) is the interop point for dio and friends |
enabled: false | Enabled.no |
enabled: () => bool | Enabled.when((query) => …) |
skipToken | Enabled.no, which keeps enabled: false's meaning where the two differ: a cached query nobody observes is still refetched by refetchQueries / invalidateQueries(refetchType: RefetchType.all) |
staleTime: 0 (the default) | StaleTime.zero |
staleTime: 30_000 | StaleTime.duration(Duration(seconds: 30)) |
staleTime: Infinity | StaleTime.infinite — never stale by time, still refetched when asked |
staleTime: 'static' | StaleTime.static — never stale and, while an observer holds the query, skipped by every refetch trigger |
staleTime: (query) => … | StaleTime.dynamic((query) => …) |
gcTime: 300_000 | GcTime.duration(Duration(minutes: 5)) (GcTime.defaultValue) |
gcTime: Infinity | GcTime.never |
retry: 3 | RetryPolicy.times(3) |
retry: false / retry: true | RetryPolicy.never / RetryPolicy.always |
retry: (count, error) => bool | RetryPolicy.when((failureCount, error, stackTrace) => …) |
retryDelay: 1000 | RetryDelay.fixed(Duration(seconds: 1)); the default backoff is RetryDelay.exponential() |
refetchOnWindowFocus: false | RefetchOn.never |
refetchOnWindowFocus: true | RefetchOn.ifStale |
refetchOnWindowFocus: 'always' | RefetchOn.always |
refetchOnMount, refetchOnReconnect | the same RefetchOn values |
refetchInterval: 5000 | RefetchInterval.every(Duration(seconds: 5)) |
refetchInterval: false | RefetchInterval.off |
refetchInterval: (query) => … | RefetchInterval.dynamic((query) => …) |
networkMode: 'online' | NetworkMode.online (also always, offlineFirst) |
initialData: value | InitialData.value(value) |
initialData: () => value | undefined | InitialData.compute(() => …); returning null means "none", while InitialData.value(null) is a value of null |
initialDataUpdatedAt: number | initialDataUpdatedAt: DateTime? |
initialDataUpdatedAt: () => number | initialDataUpdatedAtCompute: () => DateTime? — evaluated only when data is actually seeded; null means now. Give one form or the other, never both |
placeholderData: value / (previous) => … | PlaceholderData.value(…) / PlaceholderData.compute(…) |
placeholderData: keepPreviousData | const PlaceholderData.keepPrevious() |
select: (data) => … | its own options shape: QuerySelectOptions<TQueryData, TData>, select required. Without one, QueryObserverOptions<TData> has a single type argument |
notifyOnChangeProps | gone: select narrows what is reported, and every builder and keyless read takes buildWhen — a mutation's too, where there is no select |
throwOnError | gone: errors are the QueryError case of the sealed result |
structuralSharing | on by default: lists are shared element by element, maps and sets whole when deep-equal, everything else by ==, so typed models need ==/hashCode; structuralSharing: (previous, next) => … replaces it for the cache write and placeholder data; what select produced is still shared by the default comparison, because the hook is typed for the query's data and cannot be handed a selection |
structuralSharing: false | structuralSharing: noStructuralSharing() — off for the cache write, placeholder data and select output alike; a hook of your own such as (_, next) => next does not reach select |
queryCache.find({ queryKey }) | queryCache.find(filters: QueryFilters(queryKey: …)) — exact by default, as in TanStack Query; exact: false for a prefix |
Infinite queries
| JS | Here |
|---|---|
queryFn: ({ pageParam }) => page | pageFn: (context) => page, where InfinitePageContext carries pageParam and direction |
initialPageParam, getNextPageParam, getPreviousPageParam | the same names on InfiniteQueryOptions |
data.pages, data.pageParams | InfiniteData.pages, InfiniteData.pageParams |
hasNextPage, fetchNextPage() on the result | on InfiniteQueryObserver / InfiniteQueryController; the sealed result keeps one shape |
maxPages | maxPages |
Mutations
| JS | Here |
|---|---|
mutationFn: (variables, context) => … | mutationFn: (variables) => …, or mutationFnWithContext: (variables, context) => … — two fields, because Dart has no optional-arity function types. The context also carries onMutateResult and a signal |
mutate(vars, { onSuccess }) | mutate(vars, callbacks: MutateCallbacks(onSuccess: …)) |
mutateAsync(vars) | mutateAsync(vars) |
onMutate returning rollback context | onMutate returning TOnMutateResult, the observer's third type parameter |
useMutation without onMutate | MutationOptions.simple(mutationFn: …) — fixes the third type parameter to void, so the other two infer from mutationFn |
result.context (what onMutate returned, on the mutation result) | not on MutationResult: its job is the rollback, which onError and onSettled receive as their last argument |
scope: { id } | scope: MutationScope(id) |
| — | cancel() on a MutationController, observer or Mutation: fails the run with a CancelledError — see cancelling a mutation |
Caches and events
| JS | Here |
|---|---|
new QueryCache({ onError, onSuccess, onSettled }) | QueryCache(onError: …, onSuccess: …, onSettled: …) — final constructor arguments; see global callbacks |
new MutationCache({ onMutate, onSuccess, onError, onSettled }) | MutationCache(…), the same four |
queryCache.subscribe(listener) | queryCache.subscribe((event) => …); the event is a sealed QueryCacheEvent — QueryAdded, QueryRemoved, QueryUpdated, … |
meta | meta, an Object?, on query and mutation options; context.meta in the function, query.meta in the cache callbacks |
See it running
Every row above has a screen in the showcase that shows the behaviour, with widget tests and end-to-end tests that prove it. Open the app and pick the feature, or read the screen's file — each starts with what it shows and how it is proven:
| Topic | Screen |
|---|---|
| reading a query, the four call styles | simple, four-call-styles |
select, buildWhen, structural sharing | select-and-sharing, build-when |
initialData, placeholderData | initial-and-placeholder |
staleTime, gcTime | stale-and-gc, cache-inspector |
enabled | dependent-queries |
| the client's imperative surface, filters | invalidation-and-filters, prefetching, default-query-function |
| infinite queries | load-more, max-pages; pagination for the page-numbered shape |
| mutations, optimistic updates | mutations, optimistic-updates, playground |
cancelling a mutation, mutationFnWithContext | mutation-cancel |
| retries, cancellation | retry, cancellation |
refetchInterval, focus, online | auto-refetching, focus-refetch, offline |
cache callbacks, meta | global-callbacks |
a list of queries (useQueries) | query-collections, parallel-queries |
useQueries' combine | combine |
cache-wide mutation state (useMutationState) | mutation-state |
the provider's knobs — QueryClientProvider.create, isAppShown, onlineStatus, maybeOf | focus-refetch |
the errors the port adds — QueryDataTypeError, MissingMutationFunctionError | diagnostics |
Not here at all
- Suspense. React-only. The sealed result is the answer: a
switchoverQueryPending/QuerySuccess/QueryErrorputs the loading and error states in the same place as the data, with no boundary to set up. - SSR and hydration. There is no server rendering in Flutter, so
isServer,dehydrate/hydrateandHydrationBoundaryhave nothing to do. - Devtools. None. The cache is observable, though: inspecting the
cache shows how to watch it, and the showcase's
cache-inspectorscreen is a small inspector built that way. - Persistence. Not in 1.0. A persister would restore entries with
setQueryDataat start-up and save them from aqueryCache.subscribelistener; there is no ready-made one. streamedQuery. Not ported. A stream subscription that writes into the cache withsetQueryData, or invalidates it, does the same job.
The feature matrix lists everything that is out.
See also
- The overview, a shorter introduction, then installation and the quick start if you have not set a project up yet.
- The cookbook for tasks that mix several of these names, such as a token refresh or an offline-first app.
- The API reference for the Dart side of every row.