useQuery
The useQuery hook is the primary way to fetch data in the hooks adapter. It provides a declarative API for managing query state.
Basic Usage
Section titled “Basic Usage”import 'package:flutter_hooks/flutter_hooks.dart';import 'package:fasq_hooks/fasq_hooks.dart';
class UsersScreen extends HookWidget { @override Widget build(BuildContext context) { final usersState = useQuery('users'.toQueryKey(), () => api.fetchUsers());
if (usersState.isLoading) return CircularProgressIndicator(); if (usersState.hasError) return Text('Error: ${usersState.error}'); if (usersState.hasData) return UserList(users: usersState.data!);
return SizedBox(); }}Parameters
Section titled “Parameters”Required Parameters
Section titled “Required Parameters”queryKey- Unique identifier for the query (QueryKey)queryFn- Async function that fetches the data
Optional Parameters
Section titled “Optional Parameters”options- QueryOptions for configuration
Return Value
Section titled “Return Value”Returns a QueryState<T> object with:
class QueryState<T> { final T? data; // The fetched data final Object? error; // The error if any final StackTrace? stackTrace; // Stack trace for errors final QueryStatus status; // Current status: idle, loading, success, or error final bool isLoading; // True when loading final bool hasData; // True when data is available final bool hasError; // True when error occurred final bool isSuccess; // True when successfully loaded final bool isFetching; // True when refetching in background}Configuration Options
Section titled “Configuration Options”Configure query behavior with QueryOptions:
final usersState = useQuery( 'users'.toQueryKey(), () => api.fetchUsers(), options: QueryOptions( staleTime: Duration(minutes: 5), // Fresh for 5 minutes cacheTime: Duration(minutes: 10), // Keep in cache for 10 minutes enabled: true, // Whether to execute the query onSuccess: (users) { print('Users fetched: ${users.length}'); }, onError: (error) { print('Error fetching users: $error'); }, ),);Conditional Queries
Section titled “Conditional Queries”Disable queries based on conditions:
class UserProfile extends HookWidget { final String? userId;
const UserProfile({this.userId});
@override Widget build(BuildContext context) { final userState = useQuery( 'user:$userId'.toQueryKey(), () => api.fetchUser(userId!), options: QueryOptions( enabled: userId != null, // Only fetch when userId is available ), );
if (userId == null) { return Text('Please select a user'); }
if (userState.isLoading) return CircularProgressIndicator(); if (userState.hasData) return UserDetails(userState.data!);
return SizedBox(); }}Parameterized Queries
Section titled “Parameterized Queries”Use dynamic query keys for parameterized queries:
class UserProfile extends HookWidget { final String userId;
const UserProfile({required this.userId});
@override Widget build(BuildContext context) { final userState = useQuery( 'user:$userId'.toQueryKey(), // Include parameter in key () => api.fetchUser(userId), );
if (userState.isLoading) return CircularProgressIndicator(); if (userState.hasData) return UserDetails(userState.data!);
return SizedBox(); }}Dependent Queries
Section titled “Dependent Queries”Create queries that depend on other queries:
class UserPosts extends HookWidget { final String userId;
const UserPosts({required this.userId});
@override Widget build(BuildContext context) { // First fetch user final userState = useQuery( 'user:$userId'.toQueryKey(), () => api.fetchUser(userId), );
// Then fetch posts when user is available final postsState = useQuery( 'posts:user:$userId'.toQueryKey(), () => api.fetchUserPosts(userId), options: QueryOptions( enabled: userState.hasData, // Only fetch when user is loaded ), );
if (userState.isLoading) return CircularProgressIndicator(); if (!userState.hasData) return Text('User not found');
if (postsState.isLoading) return Text('Loading posts...'); if (postsState.hasData) return PostsList(postsState.data!);
return SizedBox(); }}Manual Refetch
Section titled “Manual Refetch”Trigger manual refetches:
class UserScreen extends HookWidget { @override Widget build(BuildContext context) { final usersState = useQuery('users'.toQueryKey(), () => api.fetchUsers()); final queryClient = useQueryClient();
return Column( children: [ ElevatedButton( onPressed: () { // Manually refetch the query queryClient.getQueryByKey<List<User>>('users'.toQueryKey())?.fetch(); }, child: Text('Refresh'), ), if (usersState.hasData) UserList(users: usersState.data!), ], ); }}Error Handling
Section titled “Error Handling”Handle errors with retry functionality:
class UsersScreen extends HookWidget { @override Widget build(BuildContext context) { final usersState = useQuery( 'users'.toQueryKey(), () => api.fetchUsers(), options: QueryOptions( onError: (error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $error')), ); }, ), );
if (usersState.hasError) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Error: ${usersState.error}'), ElevatedButton( onPressed: () { // Retry the query useQueryClient().getQueryByKey<List<User>>('users')?.fetch(); }, child: Text('Retry'), ), ], ); }
if (usersState.hasData) return UserList(users: usersState.data!); return CircularProgressIndicator(); }}Background Refetching
Section titled “Background Refetching”Handle background refetching with isFetching:
class UsersScreen extends HookWidget { @override Widget build(BuildContext context) { final usersState = useQuery('users'.toQueryKey(), () => api.fetchUsers());
return Column( children: [ if (usersState.isFetching && !usersState.isLoading) LinearProgressIndicator(), // Background refresh indicator if (usersState.hasData) Expanded(child: UserList(users: usersState.data!)), ], ); }}Type Safety
Section titled “Type Safety”Full generic type support ensures compile-time safety:
class UsersScreen extends HookWidget { @override Widget build(BuildContext context) { // usersState.data is List<User>? final usersState = useQuery<List<User>>('users', () => api.fetchUsers());
if (usersState.hasData) { return ListView.builder( itemCount: usersState.data!.length, itemBuilder: (context, index) { final user = usersState.data![index]; // user is User return UserTile(user); }, ); }
return CircularProgressIndicator(); }}Performance Tips
Section titled “Performance Tips”- Use descriptive query keys - Makes debugging easier
- Include parameters in keys - Enables proper caching
- Configure staleTime - Reduces unnecessary refetches
- Handle loading states - Provide good user experience
- Use error boundaries - Graceful error handling
Common Patterns
Section titled “Common Patterns”Loading Skeleton
Section titled “Loading Skeleton”class UsersScreen extends HookWidget { @override Widget build(BuildContext context) { final usersState = useQuery<List<User>>('users', () => api.fetchUsers());
if (usersState.isLoading) { return ListView.builder( itemCount: 5, itemBuilder: (context, index) => UserTileSkeleton(), ); }
if (usersState.hasData) { return ListView.builder( itemCount: usersState.data!.length, itemBuilder: (context, index) => UserTile(usersState.data![index]), ); }
return SizedBox(); }}Empty State
Section titled “Empty State”class UsersScreen extends HookWidget { @override Widget build(BuildContext context) { final usersState = useQuery<List<User>>('users', () => api.fetchUsers());
if (usersState.isLoading) return CircularProgressIndicator(); if (usersState.hasError) return Text('Error: ${usersState.error}');
if (usersState.hasData) { if (usersState.data!.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.people, size: 64, color: Colors.grey), SizedBox(height: 16), Text('No users found'), ], ), ); }
return ListView.builder( itemCount: usersState.data!.length, itemBuilder: (context, index) => UserTile(usersState.data![index]), ); }
return SizedBox(); }}Next Steps
Section titled “Next Steps”- useMutation - Learn about mutations
- useQueryClient - Cache management
- Custom Hooks - Creating reusable logic
- Examples - Complete working examples