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.

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. The binding offers four equal alternatives — there is no recommended default, pick per situation:

JSHere
useQuery(options) in a componentQueryBuilder<T>(options: …, builder: …)
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.infiniteQuery
useMutationMutationBuilder / MutationController / watchMutation / context.mutation
useQueryClient() outside a build (a handler)QueryClientProvider.read(context) — the same client, without subscribing
useQueryClient()QueryClientProvider.of(context)
QueryClientProviderQueryClientProvider(client: …, child: …)
new QueryObserver(client, options)client.observe(options) or QueryObserver(client, options)

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
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
useMutationState({ filters, select })MutationStateObserver / MutationStateController
useIsFetching(filters)IsFetchingController(client, filters: …), a ValueListenable<int>; the snapshot is client.isFetching()
useIsMutating(filters)a MutationStateController filtered on MutationStatus.pending, read for its length; the snapshot is client.isMutating()
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.