Flutter Query

Flutter Query

Coming from TanStack Query

Differences and the reasoning behind its design decisions

If you haven't used TanStack Query, feel free to skip this page. Everything you need to know is covered in the rest of the documentation.

Flutter Query is directly inspired by TanStack Query, one of the most popular data-fetching libraries in the JavaScript/TypeScript ecosystem. If you've used TanStack Query before, you'll find Flutter Query familiar. The core concepts translate directly: query keys, stale-while-revalidate patterns, automatic caching, and background refetching all work the same way conceptually.

However, there are some intentional differences. These stem from two sources: adapting to Dart and Flutter idioms, and design decisions about naming conventions. This page explains what's different and why.

Query key comparison

TanStack Query serializes keys to JSON and compares the resulting strings. Two keys are equal if their JSON representations match.

Flutter Query uses Dart's native == operator with deep equality comparison. Two keys are equal if every element compares equal using ==.

This means TanStack Query keys must be JSON-serializable (primitives, arrays, plain objects), while Flutter Query keys can contain any Dart object that implements proper equality. Class instances, enums, and other Dart types work naturally without serialization.

// This works in Flutter Query but not TanStack Query
['todos', TodoFilter(status: 'done', page: 1)]

For class instances, override == and hashCode to enable value equality. See Query Keys for details.

Duration-based options

TanStack Query uses integers in milliseconds for time-based options like staleTime and gcTime. JavaScript and TypeScript have no standard way to represent a period of time, so milliseconds is the convention.

Flutter Query uses Dart's Duration type through dedicated wrapper classes. Dart's core library provides Duration to represent time differences, and it's widely adopted throughout the Flutter ecosystem. Using Duration aligns with how time is handled elsewhere in Flutter.

The option names are also suffixed with "Duration" instead of "Time" to explicitly indicate the type:

TanStack QueryFlutter Query
staleTimestaleDuration
gcTimegcDuration

Flutter Query provides dedicated types for these durations:

// Stale duration examples
staleDuration: const StaleDuration(minutes: 5)
staleDuration: StaleDuration.infinity  // Never becomes stale
staleDuration: StaleDuration.static    // Immutable, ignores invalidation

// Garbage collection duration examples
gcDuration: const GcDuration(minutes: 10)
gcDuration: GcDuration.infinity  // Never garbage collected

These wrapper types offer semantic clarity and provide special values like infinity and static that wouldn't be possible with raw Duration.

Renamed options

Some options are renamed to reduce verbosity:

TanStack QueryFlutter Query
placeholderDataplaceholder
initialDataseed
initialDataUpdatedAtseedUpdatedAt

The reasoning behind shorter names is that libraries forming the core of your codebase benefit from concise naming. Flutter Query is designed to handle most of your data-fetching needs. These options appear frequently throughout an application, and their purpose becomes well-understood by the team over time. At that point, longer descriptive names add visual noise without improving clarity.

The seed terminology also better conveys the concept: you're providing starting data that the query can grow from, not just initial data that gets replaced.

Retry options

TanStack Query uses two separate options to control retry behavior: retry for the retry count, and retryDelay for the delay between attempts.

// TanStack Query
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)

Flutter Query combines these into a single retry callback function. The function receives the retry count and error, then returns the delay before the next attempt. Returning null stops retrying.

// Flutter Query
retry: (retryCount, error) {
  if (retryCount >= 3) return null;  // Stop after 3 retries
  return Duration(seconds: 1 << retryCount);  // Exponential backoff
}

This combined approach keeps all retry logic in one place. You can base both the retry count and delay on the error type within a single function.

No callback argument for options

TanStack Query allows certain options to accept either a value or a callback function. For example, placeholderData can be a static value or a function that receives the previous query data:

// TanStack Query - static value
placeholderData: sampleData;

// TanStack Query - callback form
placeholderData: (previousData, previousQuery) => previousData;

The callback form is particularly useful for paginated queries, where you can display data from a previous page while fetching the next one, avoiding a loading spinner during transitions.

Flutter Query intentionally does not implement callback form options.

TypeScript supports union types, so TanStack Query can define an option as TData | ((previousData, previousQuery) => TData) with no friction. Dart has no union type support. To accept both forms, Flutter Query would need to either provide a sealed wrapper class or add separate parameters for the callback variant. Both approaches feel unergonomic and could confuse users unfamiliar with TanStack Query or new to the library.

While supporting callback forms would increase compatibility with TanStack Query and ease onboarding for experienced users, I decided to keep the API surface simple and Flutter-like. That said, callback support for placeholder is under consideration since the use case for dynamic placeholder data is compelling.

Summary

AspectTanStack QueryFlutter Query
Key comparisonJSON serializationDart == with deep equality
Time optionsIntegers (milliseconds)Duration via wrapper types
Time option namesstaleTime, gcTimestaleDuration, gcDuration
Placeholder dataplaceholderDataplaceholder
Initial datainitialData, initialDataUpdatedAtseed, seedUpdatedAt
Retry optionsretry, retryDelay (separate)retry (combined callback)
Callback formsSupported via union typesNot supported

If you're migrating from TanStack Query, these differences are straightforward to adapt to. The mental model remains the same. You're just writing more idiomatic Dart.

If you have thoughts on any of these design decisions, feel free to share your opinion in the GitHub issues.

On this page