Type-Safe Query Keys
Type-safe query keys provide compile-time type checking and better IDE support for your queries. This guide shows you how to use TypedQueryKey to create a fully type-safe query key system.
Why Type-Safe Query Keys?
Section titled “Why Type-Safe Query Keys?”Using type-safe query keys offers several benefits:
- Compile-time safety: Catch key mismatches at compile time
- Better IDE support: Autocomplete and refactoring support
- Type inference: Automatic type inference for query data
- Centralized management: Single source of truth for all query keys
- Refactoring safety: Rename keys across your entire codebase safely
Basic Usage
Section titled “Basic Usage”Simple String Keys
Section titled “Simple String Keys”For simple cases, you can use the toQueryKey() extension:
QueryBuilder<List<User>>( queryKey: 'users'.toQueryKey(), queryFn: () => api.fetchUsers(), builder: (context, state) => ...,)Type-Safe Query Keys
Section titled “Type-Safe Query Keys”For better type safety, use TypedQueryKey:
// Define query keys in a central locationclass QueryKeys { static TypedQueryKey<List<User>> get users => const TypedQueryKey<List<User>>('users', List<User>);
static TypedQueryKey<User> user(String id) => TypedQueryKey<User>('user:$id', User);}
// Use in queriesQueryBuilder<List<User>>( queryKey: QueryKeys.users, queryFn: () => api.fetchUsers(), builder: (context, state) => ...,)
QueryBuilder<User>( queryKey: QueryKeys.user('123'), queryFn: () => api.fetchUser('123'), builder: (context, state) => ...,)Creating a QueryKeys Class
Section titled “Creating a QueryKeys Class”Basic Structure
Section titled “Basic Structure”Create a centralized QueryKeys class to manage all your query keys:
import 'package:fasq/fasq.dart';
class QueryKeys { // Simple keys static TypedQueryKey<List<User>> get users => const TypedQueryKey<List<User>>('users', List<User>);
static TypedQueryKey<List<Post>> get posts => const TypedQueryKey<List<Post>>('posts', List<Post>);
// Parameterized keys static TypedQueryKey<User> user(String id) => TypedQueryKey<User>('user:$id', User);
static TypedQueryKey<Post> post(String id) => TypedQueryKey<Post>('post:$id', Post);
// Complex keys with multiple parameters static TypedQueryKey<List<Post>> postsByUser(String userId) => TypedQueryKey<List<Post>>('posts:user:$userId', List<Post>);
static TypedQueryKey<List<Comment>> commentsByPost(String postId) => TypedQueryKey<List<Comment>>('comments:post:$postId', List<Comment>);}Using with Parameters
Section titled “Using with Parameters”For queries that depend on parameters, create factory methods:
class QueryKeys { // Single parameter static TypedQueryKey<User> user(String id) => TypedQueryKey<User>('user:$id', User);
// Multiple parameters static TypedQueryKey<List<Post>> postsByUserAndPage( String userId, int page, ) => TypedQueryKey<List<Post>>( 'posts:user:$userId:page:$page', List<Post>, );
// Using withParam helper static TypedQueryKey<List<Post>> postsByUser(String userId) => const TypedQueryKey<List<Post>>('posts', List<Post>) .withParam(userId);}Integration with QueryClient
Section titled “Integration with QueryClient”Using with QueryClient Methods
Section titled “Using with QueryClient Methods”All QueryClient methods accept QueryKey:
final client = QueryClient();
// Get queryfinal query = client.getQuery<List<User>>( QueryKeys.users, () => api.fetchUsers(),);
// Prefetchawait client.prefetchQuery( QueryKeys.users, () => api.fetchUsers(),);
// Invalidateclient.invalidateQuery(QueryKeys.users);
// Set dataclient.setQueryData(QueryKeys.users, userList);
// Get datafinal users = client.getQueryData<List<User>>(QueryKeys.users);Integration with Widgets
Section titled “Integration with Widgets”QueryBuilder
Section titled “QueryBuilder”QueryBuilder<List<User>>( queryKey: QueryKeys.users, queryFn: () => api.fetchUsers(), builder: (context, state) { if (state.hasData) { return ListView.builder( itemCount: state.data!.length, itemBuilder: (context, index) { final user = state.data![index]; // Type: User return UserTile(user); }, ); } return CircularProgressIndicator(); },)Parameterized Queries
Section titled “Parameterized Queries”class UserProfile extends StatelessWidget { final String userId;
const UserProfile({required this.userId});
@override Widget build(BuildContext context) { return QueryBuilder<User>( queryKey: QueryKeys.user(userId), queryFn: () => api.fetchUser(userId), builder: (context, state) { if (state.hasData) { final user = state.data!; // Type: User return UserDetails(user); } return CircularProgressIndicator(); }, ); }}Integration with Adapters
Section titled “Integration with Adapters”final state = useQuery<List<User>>( QueryKeys.users, () => api.fetchUsers(),);class UsersCubit extends QueryCubit<List<User>> { @override QueryKey get queryKey => QueryKeys.users;
@override Future<List<User>> Function() get queryFn => () => api.fetchUsers();}Riverpod
Section titled “Riverpod”final usersProvider = queryProvider<List<User>>( QueryKeys.users, () => api.fetchUsers(),);Prefetching with Type-Safe Keys
Section titled “Prefetching with Type-Safe Keys”Single Prefetch
Section titled “Single Prefetch”await client.prefetchQuery( QueryKeys.users, () => api.fetchUsers(),);Multiple Prefetches
Section titled “Multiple Prefetches”await client.prefetchQueries([ PrefetchConfig( queryKey: QueryKeys.users, queryFn: () => api.fetchUsers(), ), PrefetchConfig( queryKey: QueryKeys.posts, queryFn: () => api.fetchPosts(), ),]);Best Practices
Section titled “Best Practices”1. Centralize Query Keys
Section titled “1. Centralize Query Keys”Keep all query keys in a single QueryKeys class:
// Good: Centralizedclass QueryKeys { static TypedQueryKey<List<User>> get users => ...;}
// Bad: Scatteredfinal usersKey = 'users'.toQueryKey(); // In file Afinal postsKey = 'posts'.toQueryKey(); // In file B2. Use Descriptive Names
Section titled “2. Use Descriptive Names”Make query key names descriptive and hierarchical:
// Good: Clear and hierarchicalQueryKeys.user('123')QueryKeys.postsByUser('123')QueryKeys.commentsByPost('456')
// Bad: UnclearQueryKeys.u('123')QueryKeys.p('123')3. Group Related Keys
Section titled “3. Group Related Keys”Organize related keys together:
class QueryKeys { // User-related keys static TypedQueryKey<List<User>> get users => ...; static TypedQueryKey<User> user(String id) => ...; static TypedQueryKey<List<Post>> postsByUser(String userId) => ...;
// Post-related keys static TypedQueryKey<List<Post>> get posts => ...; static TypedQueryKey<Post> post(String id) => ...; static TypedQueryKey<List<Comment>> commentsByPost(String postId) => ...;}4. Use Constants for Static Keys
Section titled “4. Use Constants for Static Keys”Use const for keys that don’t change:
class QueryKeys { // Good: const for static keys static const TypedQueryKey<List<User>> users = TypedQueryKey<List<User>>('users', List<User>);
// Good: Factory for dynamic keys static TypedQueryKey<User> user(String id) => TypedQueryKey<User>('user:$id', User);}5. Type Safety First
Section titled “5. Type Safety First”Always specify the type parameter:
// Good: Explicit typestatic TypedQueryKey<List<User>> get users => const TypedQueryKey<List<User>>('users', List<User>);
// Bad: Missing typestatic TypedQueryKey get users => ...; // Type is dynamicAdvanced Patterns
Section titled “Advanced Patterns”Nested Keys
Section titled “Nested Keys”For complex data structures, use nested keys:
class QueryKeys { static TypedQueryKey<List<User>> get users => ...;
static TypedQueryKey<User> user(String id) => ...;
static TypedQueryKey<List<Post>> postsByUser(String userId) => TypedQueryKey<List<Post>>('posts:user:$userId', List<Post>);
static TypedQueryKey<Post> postByUser(String userId, String postId) => TypedQueryKey<Post>('post:user:$userId:$postId', Post);}Key Prefixes
Section titled “Key Prefixes”Use prefixes to group related keys:
class QueryKeys { static const String _userPrefix = 'user'; static const String _postPrefix = 'post';
static TypedQueryKey<User> user(String id) => TypedQueryKey<User>('$_userPrefix:$id', User);
static TypedQueryKey<List<Post>> postsByUser(String userId) => TypedQueryKey<List<Post>>('$_postPrefix:$_userPrefix:$userId', List<Post>);}Conditional Keys
Section titled “Conditional Keys”Create keys based on conditions:
class QueryKeys { static TypedQueryKey<List<Post>> posts({ String? userId, String? category, }) { final parts = <String>['posts']; if (userId != null) parts.add('user:$userId'); if (category != null) parts.add('category:$category'); return TypedQueryKey<List<Post>>(parts.join(':'), List<Post>); }}Migration Guide
Section titled “Migration Guide”From String Keys
Section titled “From String Keys”If you’re migrating from string keys:
- Create a
QueryKeysclass - Replace string keys with
TypedQueryKeyinstances - Update all usages to use the new keys
// BeforeQueryBuilder<List<User>>( queryKey: 'users', queryFn: () => api.fetchUsers(), ...)
// AfterQueryBuilder<List<User>>( queryKey: QueryKeys.users, queryFn: () => api.fetchUsers(), ...)Gradual Migration
Section titled “Gradual Migration”You can migrate gradually:
// Temporary: Use extension for backward compatibilityQueryBuilder<List<User>>( queryKey: 'users'.toQueryKey(), queryFn: () => api.fetchUsers(), ...)
// Final: Use type-safe keysQueryBuilder<List<User>>( queryKey: QueryKeys.users, queryFn: () => api.fetchUsers(), ...)Examples
Section titled “Examples”Complete Example
Section titled “Complete Example”import 'package:fasq/fasq.dart';
class User { final String id; final String name; final String email;
User({required this.id, required this.name, required this.email});}
class Post { final String id; final String title; final String body; final String userId;
Post({ required this.id, required this.title, required this.body, required this.userId, });}
class QueryKeys { static const TypedQueryKey<List<User>> users = TypedQueryKey<List<User>>('users', List<User>);
static TypedQueryKey<User> user(String id) => TypedQueryKey<User>('user:$id', User);
static const TypedQueryKey<List<Post>> posts = TypedQueryKey<List<Post>>('posts', List<Post>);
static TypedQueryKey<List<Post>> postsByUser(String userId) => TypedQueryKey<List<Post>>('posts:user:$userId', List<Post>);
static TypedQueryKey<Post> post(String id) => TypedQueryKey<Post>('post:$id', Post);}
// Usageclass UsersScreen extends StatelessWidget { @override Widget build(BuildContext context) { return QueryBuilder<List<User>>( queryKey: QueryKeys.users, queryFn: () => api.fetchUsers(), builder: (context, state) { if (state.hasData) { return ListView.builder( itemCount: state.data!.length, itemBuilder: (context, index) { final user = state.data![index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => UserProfile(userId: user.id), ), ); }, ); }, ); } return CircularProgressIndicator(); }, ); }}
class UserProfile extends StatelessWidget { final String userId;
const UserProfile({required this.userId});
@override Widget build(BuildContext context) { return QueryBuilder<User>( queryKey: QueryKeys.user(userId), queryFn: () => api.fetchUser(userId), builder: (context, state) { if (state.hasData) { final user = state.data!; return Scaffold( appBar: AppBar(title: Text(user.name)), body: Column( children: [ Text(user.email), ElevatedButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => UserPosts(userId: user.id), ), ); }, child: Text('View Posts'), ), ], ), ); } return CircularProgressIndicator(); }, ); }}Summary
Section titled “Summary”Type-safe query keys provide:
- Compile-time type checking
- Better IDE support
- Centralized key management
- Refactoring safety
- Type inference
Start using type-safe query keys today to make your codebase more maintainable and less error-prone!