Coming from TanStack Query
Qora shares the same conceptual model as TanStack Query: query keys, stale-while-revalidate, background refetch, mutation lifecycle hooks, and offline pause-and-replay. Most concepts translate directly. This page maps what you already know to the Qora API.
Concept Map
| TanStack Query concept | Qora equivalent |
|---|---|
QueryClient | QoraClient |
QueryClientProvider | QoraScope |
useQuery | QoraBuilder<T> (widget) or useQuery<T> (hook) |
useMutation | QoraMutationBuilder (widget) or useMutation (hook) |
useInfiniteQuery | InfiniteQueryBuilder (widget) or useInfiniteQuery (hook) |
useQueryClient | context.qora / useQueryClient() (hook) |
useIsFetching | useIsFetching() (hook) or client.fetchingCountStream |
useIsMutating | useIsMutating() (hook) or client.activeMutations |
QueryObserver | client.watchQuery<T> / InfiniteQueryObserver |
persistQueryClient | PersistQoraClient |
DevTools | qora_devtools_overlay (in-app) |
API Reference: Queries
Options
| TanStack Query option | Qora equivalent | Notes |
|---|---|---|
queryKey | queryKey | Same concept. TanStack uses arrays; Qora uses List<dynamic>. Structural equality in both. |
queryFn | fetcher | Same role. Renamed. |
enabled | enabled | Identical. |
staleTime | staleTime | Identical. Duration.zero = always stale. |
gcTime (cacheTime in v4) | gcTime | Now matches v5 naming. |
retry | retryCount | Renamed. Default is 3 in both. |
retryDelay | retryDelay / retryDelayCalculator | Exponential backoff is the default. retryDelayCalculator accepts a function receiving the attempt index. |
refetchOnWindowFocus | refetchOnWindowFocus | Identical. Requires FlutterLifecycleManager. |
refetchOnReconnect | refetchOnReconnect | Identical. Requires FlutterConnectivityManager. |
refetchOnMount | refetchOnMount | Identical semantics. null falls back to QoraClientConfig.refetchOnMount. |
refetchInterval | refetchInterval | Identical. Active while at least one subscriber is present. |
networkMode | networkMode | Same name, same three values: online, always, offlineFirst. |
select | select (rebuild filter) / transform / state.map() | Different semantics, see below. |
placeholderData | placeholderData | A function returning a placeholder, for example from another cached query. |
placeholderData: keepPreviousData | previousData on Loading / Failure, or keepPreviousData: true | previousData is always present. keepPreviousData keeps the entry in Success during a refetch. |
initialData | initialData | Seeded before the first frame. client.hydrateQuery<T>(key, data) does the same imperatively. |
initialDataUpdatedAt | initialDataUpdatedAt | Defaults to the epoch, so initial data is stale and refetched on mount. |
structuralSharing | structuralSharing | Enabled by default. Unchanged nested data keeps its identity across fetches. |
notifyOnChangeProps | Not available | All state transitions emit on the stream. |
Return values
| TanStack Query field | Qora equivalent |
|---|---|
status: 'pending' | state is Initial or state is Loading(previousData: null) |
status: 'error' | state is Failure |
status: 'success' | state is Success |
fetchStatus: 'fetching' | fetchStatus == FetchStatus.fetching |
fetchStatus: 'paused' | fetchStatus == FetchStatus.paused |
fetchStatus: 'idle' | fetchStatus == FetchStatus.idle |
isLoading | state.isLoading |
isFetching | fetchStatus == FetchStatus.fetching |
isError | state.isError |
isSuccess | state.isSuccess |
isPending | state is Initial || state is Loading(previousData: null) |
data | state.dataOrNull or Success(:final data) via pattern match |
error | state.errorOrNull or Failure(:final error) via pattern match |
dataUpdatedAt | state.updatedAt (extension, non-null only on Success) |
errorUpdatedAt | Not available as a separate field |
isFetchedAfterMount | Not available |
API Reference: Mutations
Options
| TanStack Query option | Qora equivalent | Notes |
|---|---|---|
mutationFn | mutator | Renamed. |
onMutate | MutationOptions.onMutate | Same role. Return value becomes TContext. |
onSuccess | MutationOptions.onSuccess | Same role. Signature: (TData, TVariables, TContext?). |
onError | MutationOptions.onError | Same role. Signature: (Object, TVariables, TContext?). |
onSettled | MutationOptions.onSettled | Same role. Signature: (TData?, Object?, TVariables, TContext?). |
retry | MutationOptions.retryCount | Renamed. Default is 0 (no retry). |
retryDelay | MutationOptions.retryDelay | Exponential backoff. |
networkMode | MutationOptions.networkMode | Same values. Add offlineQueue: true to queue writes made offline and replay them on reconnect. |
variables | TVariables type parameter | Same concept. |
context (rollback snapshot) | TContext type parameter | Same concept. Returned by onMutate, forwarded to onError/onSettled. |
Return values / state
| TanStack Query field | Qora equivalent |
|---|---|
status: 'idle' | state is MutationIdle |
status: 'pending' | state is MutationPending |
status: 'error' | state is MutationFailure |
status: 'success' | state is MutationSuccess |
isIdle | state.isIdle |
isPending | state.isPending |
isError | state.isError |
isSuccess | state.isSuccess |
data | state.dataOrNull |
error | state.errorOrNull |
variables | state.variablesOrNull |
mutate(variables) | mutate(variables): fire-and-forget, errors captured in state |
mutateAsync(variables) | mutateAsync(variables): awaitable, errors propagated |
reset() | reset(): transitions back to MutationIdle |
API Reference: QueryClient → QoraClient
| TanStack Query method | Qora equivalent |
|---|---|
invalidateQueries({ queryKey }) | client.invalidate(key) |
invalidateQueries(filter) | client.invalidateWhere(predicate) or client.invalidateQueries(filter) |
setQueryData(queryKey, updater) | client.setQueryData<T>(key, data) |
getQueryData(queryKey) | client.getQueryData<T>(key): returns T? |
getQueryState(queryKey) | client.getQueryState<T>(key): returns QoraState<T> |
prefetchQuery | client.prefetch<T>(key, fetcher, options) |
resetQueries | client.removeQuery(key) or client.clear() |
cancelQueries | Not available in Qora v1 |
isFetching(queryKey) | client.isFetchingCount (global count) |
setDefaultOptions | QoraClientConfig.defaultOptions (set at construction) |
getDefaultOptions | client.config.defaultOptions |
mount / unmount | Handled by QoraScope / widget lifecycle |
clear() | client.clear() |
Key Differences
Sealed classes replace status strings
TanStack Query returns plain objects with string status fields and boolean shortcuts (isLoading, isError, etc.). Qora returns a sealed class QoraState<T>. Dart 3 pattern matching makes exhaustive handling compile-time enforced:
// TanStack Query (React)
if (isLoading) return <Spinner />;
if (isError) return <Error error={error} />;
return <UserCard user={data} />;
// Qora (Dart 3)
return switch (state) {
Initial() || Loading(previousData: null) => const CircularProgressIndicator(),
Failure(:final error, previousData: null) => ErrorScreen(error),
_ => UserCard(state.dataOrNull!),
};
The _ arm covers Success, Loading(previousData != null), and Failure(previousData != null): all cases where data is available.
previousData is always present
TanStack Query v5 keeps the previous data with placeholderData: keepPreviousData. In Qora, Loading<T> and Failure<T> always carry previousData: T?, so stale data stays accessible during background refreshes and after errors without any option.
// Show stale data with a progress indicator during a background refresh
case Loading(:final previousData) when previousData != null:
return Stack(children: [
UserCard(previousData),
const LinearProgressIndicator(minHeight: 2),
]);
If you prefer the entry to stay in Success while it refetches, for example to keep a paginated table stable, set keepPreviousData: true.
select filters rebuilds, it does not transform
In TanStack Query, select transforms the data returned to the component. In Qora, select on QoraBuilder and useQuery decides when the widget rebuilds: only when the state kind or the selected value changes. The state you receive is always the full one.
To transform data, Qora offers two tools:
// TanStack Query
useQuery({ queryKey, queryFn, select: (data) => data.name });
// Qora: transform once, in the cache (every subscriber sees the result)
QoraOptions(transform: (raw) => User.fromJson(raw as Map<String, dynamic>));
// Qora: transform per consumer, on the state
final nameState = userState.map((user) => user.name);
// QoraState<User> → QoraState<String>, previousData included
// Qora: rebuild only when the name changes
QoraBuilder<User>(
queryKey: ['users', id],
fetcher: () => api.getUser(id),
select: (user) => user.name,
builder: (context, state, _) => Text(state.dataOrNull?.name ?? ''),
);
gcTime replaces cacheTime
Qora uses the TanStack Query v5 name. gcTime is the time an inactive entry stays in memory after its last subscriber cancels.
Widget API vs hook API
React Query's primary API is hooks (useQuery, useMutation). Qora provides both a widget API and a hook API:
QoraBuilder<T>andQoraMutationBuilderwork in anyStatelessWidgetorStatefulWidget.useQuery<T>anduseMutationrequireHookWidgetfromflutter_hooks.
Both share the same QoraClient cache. If a QoraBuilder already fetched ['users', id], useQuery gets it on the first frame from the same entry.
QoraScope is a StatefulWidget, not a Provider
TanStack Query uses QueryClientProvider (React context). Qora uses QoraScope, a StatefulWidget that exposes the client to its subtree via an internal InheritedWidget. Access the client from any descendant via context.qora or QoraScope.of(context).
Persistence requires explicit serializer registration
TanStack Query's persistence plugins (e.g. createSyncStoragePersister) use JSON.stringify / JSON.parse on the full query cache. Qora requires explicit serializer registration per type because Dart has no runtime reflection:
// Required before hydrate()
client.registerSerializer<User>(
QoraSerializer(toJson: (u) => u.toJson(), fromJson: User.fromJson),
name: 'User',
);
await client.hydrate();
The name string must be stable across app versions and build modes (including obfuscated builds).
No React Suspense equivalent
TanStack Query supports suspense: true to integrate with React Suspense. Flutter has no equivalent mechanism. All async state is handled through QoraState<T> pattern matching in the widget tree.
Infinite Queries
| TanStack Query | Qora |
|---|---|
useInfiniteQuery | InfiniteQueryBuilder / useInfiniteQuery |
queryFn({ pageParam }) | fetcher(TPageParam pageParam) |
initialPageParam | InfiniteQueryOptions.initialPageParam |
getNextPageParam(lastPage, pages) | InfiniteQueryOptions.getNextPageParam(lastPage, pages) |
getPreviousPageParam | InfiniteQueryOptions.getPreviousPageParam |
maxPages | InfiniteQueryOptions.maxPages |
fetchNextPage() | observer.fetchNextPage() |
fetchPreviousPage() | observer.fetchPreviousPage() |
hasNextPage | InfiniteSuccess.hasNextPage |
hasPreviousPage | InfiniteSuccess.hasPreviousPage |
isFetchingNextPage | InfiniteSuccess.isFetchingNextPage |
isFetchingPreviousPage | InfiniteSuccess.isFetchingPreviousPage |
data.pages | InfiniteData.pages |
data.pageParams | InfiniteData.pageParams |
data.pages.flat() | InfiniteData.flatten((page) => page.items) |
queryClient.setQueryData (infinite) | client.setInfiniteQueryData<TData, TPageParam>(key, InfiniteData) |
queryClient.getQueryData (infinite) | client.getInfiniteQueryData<TData, TPageParam>(key) |
DevTools
TanStack Query DevTools is a browser panel installed as a React component. Qora's equivalent is an in-app overlay: install qora_devtools_overlay and wrap the app with QoraInspector. It shows queries, mutations and a timeline, and renders nothing in release builds.
See DevTools overview for setup instructions.
Related
- What is Qora: conceptual overview
- Quick Start: five-minute setup
- QoraState: sealed class reference
- QoraOptions: full per-query option reference
- QoraClientConfig: global configuration reference