Skip to main content

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:

  • null means "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 a switch is exhaustive and there is no data!. A QueryError still carries staleData.

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.

JSThe bindingWith query_kit_hooks
useQuery(options) in a componentQueryBuilder<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
useInfiniteQueryInfiniteQueryBuilder / InfiniteQueryController / watchInfiniteQuery / context.infiniteQueryuseInfiniteQuery(options), which returns the InfiniteQueryController
useMutationMutationBuilder / MutationController / watchMutation / context.mutationuseMutation(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 resultsuseQueries(queries), one data type per list as well
useQueries({ queries, combine })combine over the list of results, see combining queriesuseCombinedQueries(queries, combiner, keys: […])
useMutationState({ filters, select })MutationStateObserver / MutationStateControlleruseMutationState(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, usePrefetchInfiniteQueryclient.query(options).ignore(), as under the clientusePrefetchQuery(options), usePrefetchInfiniteQuery(options)
useQueryClient() outside a build (a handler)QueryClientProvider.read(context) — the same client, without subscribingthe same
useQueryClient()QueryClientProvider.of(context)useQueryClient()
QueryClientProviderQueryClientProvider(client: …, child: …)the same
new QueryObserver(client, options)client.observe(options) or QueryObserver(client, options)the same

The client​

JSHere
fetchQuery, prefetchQuery, ensureQueryDataone 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: …))
queryKeyHashFngone: 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​

JSHere
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: falseEnabled.no
enabled: () => boolEnabled.when((query) => …)
skipTokenEnabled.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_000StaleTime.duration(Duration(seconds: 30))
staleTime: InfinityStaleTime.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_000GcTime.duration(Duration(minutes: 5)) (GcTime.defaultValue)
gcTime: InfinityGcTime.never
retry: 3RetryPolicy.times(3)
retry: false / retry: trueRetryPolicy.never / RetryPolicy.always
retry: (count, error) => boolRetryPolicy.when((failureCount, error, stackTrace) => …)
retryDelay: 1000RetryDelay.fixed(Duration(seconds: 1)); the default backoff is RetryDelay.exponential()
refetchOnWindowFocus: falseRefetchOn.never
refetchOnWindowFocus: trueRefetchOn.ifStale
refetchOnWindowFocus: 'always'RefetchOn.always
refetchOnMount, refetchOnReconnectthe same RefetchOn values
refetchInterval: 5000RefetchInterval.every(Duration(seconds: 5))
refetchInterval: falseRefetchInterval.off
refetchInterval: (query) => …RefetchInterval.dynamic((query) => …)
networkMode: 'online'NetworkMode.online (also always, offlineFirst)
initialData: valueInitialData.value(value)
initialData: () => value | undefinedInitialData.compute(() => …); returning null means "none", while InitialData.value(null) is a value of null
initialDataUpdatedAt: numberinitialDataUpdatedAt: DateTime?
initialDataUpdatedAt: () => numberinitialDataUpdatedAtCompute: () => 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: keepPreviousDataconst PlaceholderData.keepPrevious()
select: (data) => …its own options shape: QuerySelectOptions<TQueryData, TData>, select required. Without one, QueryObserverOptions<TData> has a single type argument
notifyOnChangePropsgone: select narrows what is reported, and every builder and keyless read takes buildWhen — a mutation's too, where there is no select
throwOnErrorgone: errors are the QueryError case of the sealed result
structuralSharingon 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: falsestructuralSharing: 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​

JSHere
queryFn: ({ pageParam }) => pagepageFn: (context) => page, where InfinitePageContext carries pageParam and direction
initialPageParam, getNextPageParam, getPreviousPageParamthe same names on InfiniteQueryOptions
data.pages, data.pageParamsInfiniteData.pages, InfiniteData.pageParams
hasNextPage, fetchNextPage() on the resulton InfiniteQueryObserver / InfiniteQueryController; the sealed result keeps one shape
maxPagesmaxPages

Mutations​

JSHere
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 contextonMutate returning TOnMutateResult, the observer's third type parameter
useMutation without onMutateMutationOptions.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​

JSHere
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, …
metameta, 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:

TopicScreen
reading a query, the four call stylessimple, four-call-styles
select, buildWhen, structural sharingselect-and-sharing, build-when
initialData, placeholderDatainitial-and-placeholder
staleTime, gcTimestale-and-gc, cache-inspector
enableddependent-queries
the client's imperative surface, filtersinvalidation-and-filters, prefetching, default-query-function
infinite queriesload-more, max-pages; pagination for the page-numbered shape
mutations, optimistic updatesmutations, optimistic-updates, playground
cancelling a mutation, mutationFnWithContextmutation-cancel
retries, cancellationretry, cancellation
refetchInterval, focus, onlineauto-refetching, focus-refetch, offline
cache callbacks, metaglobal-callbacks
a list of queries (useQueries)query-collections, parallel-queries
useQueries' combinecombine
cache-wide mutation state (useMutationState)mutation-state
the provider's knobs — QueryClientProvider.create, isAppShown, onlineStatus, maybeOffocus-refetch
the errors the port adds — QueryDataTypeError, MissingMutationFunctionErrordiagnostics

Not here at all​

  • Suspense. React-only. The sealed result is the answer: a switch over QueryPending / QuerySuccess / QueryError puts 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/hydrate and HydrationBoundary have nothing to do.
  • Devtools. None. The cache is observable, though: inspecting the cache shows how to watch it, and the showcase's cache-inspector screen is a small inspector built that way.
  • Persistence. Not in 1.0. A persister would restore entries with setQueryData at start-up and save them from a queryCache.subscribe listener; there is no ready-made one.
  • streamedQuery. Not ported. A stream subscription that writes into the cache with setQueryData, or invalidates it, does the same job.

The feature matrix lists everything that is out.

See also​