Skip to content

useQuery

The useQuery hook is the primary way to fetch data in the hooks adapter. It provides a declarative API for managing query state.

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();
}
}
  • queryKey - Unique identifier for the query (QueryKey)
  • queryFn - Async function that fetches the data
  • options - QueryOptions for configuration

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
}

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');
},
),
);

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();
}
}

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();
}
}

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();
}
}

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!),
],
);
}
}

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();
}
}

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!)),
],
);
}
}

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();
}
}
  1. Use descriptive query keys - Makes debugging easier
  2. Include parameters in keys - Enables proper caching
  3. Configure staleTime - Reduces unnecessary refetches
  4. Handle loading states - Provide good user experience
  5. Use error boundaries - Graceful error handling
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();
}
}
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();
}
}