useQueries Hook
Execute multiple queries in parallel with the Hooks adapter.
Basic Usage
Section titled “Basic Usage”import 'package:fasq_hooks/fasq_hooks.dart';
class Dashboard extends HookWidget { @override Widget build(BuildContext context) { final queries = useQueries([ QueryConfig('users', () => api.fetchUsers()), QueryConfig('posts', () => api.fetchPosts()), QueryConfig('comments', () => api.fetchComments()), ]);
return Column( children: [ UsersList(queries[0]), PostsList(queries[1]), CommentsList(queries[2]), ], ); }}API Reference
Section titled “API Reference”useQueries
Section titled “useQueries”List<QueryState<dynamic>> useQueries(List<QueryConfig> configs)Parameters:
configs: List of query configurations
Returns:
List<QueryState<dynamic>>: Array of query states corresponding to each config
QueryConfig
Section titled “QueryConfig”class QueryConfig<T> { final String key; final Future<T> Function() queryFn; final QueryOptions? options;
const QueryConfig(this.key, this.queryFn, {this.options});}Parameters:
key: Unique identifier for the queryqueryFn: Function that returns a Future with the dataoptions: Optional query configuration
State Management
Section titled “State Management”Checking Aggregate States
Section titled “Checking Aggregate States”final queries = useQueries(configs);
// All queries have datafinal allLoaded = queries.every((q) => q.hasData);
// Any query is loadingfinal anyLoading = queries.any((q) => q.isLoading);
// Any query has errorfinal hasError = queries.any((q) => q.hasError);
// All queries successfulfinal allSuccess = queries.every((q) => q.isSuccess);Accessing Individual States
Section titled “Accessing Individual States”final queries = useQueries(configs);
// Access by indexfinal userState = queries[0];final postState = queries[1];final commentState = queries[2];
// Type-safe accessfinal userState = queries[0] as QueryState<List<User>>;final postState = queries[1] as QueryState<List<Post>>;Named Queries with useNamedQueries
Section titled “Named Queries with useNamedQueries”For better developer experience, you can use named queries with map-based access:
import 'package:fasq_hooks/fasq_hooks.dart';
class Dashboard extends HookWidget { @override Widget build(BuildContext context) { final queries = useNamedQueries([ NamedQueryConfig(name: 'users', key: 'users', queryFn: () => api.fetchUsers()), NamedQueryConfig(name: 'posts', key: 'posts', queryFn: () => api.fetchPosts()), NamedQueryConfig(name: 'comments', key: 'comments', queryFn: () => api.fetchComments()), ]);
final allLoaded = queries.values.every((q) => q.hasData); final anyError = queries.values.any((q) => q.hasError);
return Column( children: [ if (!allLoaded) LinearProgressIndicator(), if (anyError) ErrorBanner(), UsersList(queries['users']!), PostsList(queries['posts']!), CommentsList(queries['comments']!), ], ); }}NamedQueryConfig
Section titled “NamedQueryConfig”The NamedQueryConfig class provides configuration for named queries:
class NamedQueryConfig<T> { final String name; // Name identifier for this query final String key; // Unique identifier for this query final Future<T> Function() queryFn; // Function that returns a Future with the data final QueryOptions? options; // Optional configuration for this query}Benefits of Named Access
Section titled “Benefits of Named Access”- Better DX: Access queries by meaningful names instead of indices
- Type Safety: Compile-time checking for query names
- Self-Documenting: Code is more readable and maintainable
- Refactoring Safe: Renaming queries updates all references
Advanced Patterns
Section titled “Advanced Patterns”Conditional Queries
Section titled “Conditional Queries”class ConditionalDashboard extends HookWidget { final bool loadComments;
const ConditionalDashboard({required this.loadComments});
@override Widget build(BuildContext context) { final configs = [ QueryConfig('users', () => api.fetchUsers()), QueryConfig('posts', () => api.fetchPosts()), if (loadComments) QueryConfig('comments', () => api.fetchComments()), ];
final queries = useQueries(configs);
return Column( children: [ UsersList(queries[0]), PostsList(queries[1]), if (loadComments) CommentsList(queries[2]), ], ); }}Dynamic Query Lists
Section titled “Dynamic Query Lists”class DynamicDashboard extends HookWidget { final List<String> userIds;
const DynamicDashboard({required this.userIds});
@override Widget build(BuildContext context) { final configs = userIds.map((id) => QueryConfig('user-$id', () => api.fetchUser(id)) ).toList();
final queries = useQueries(configs);
return Column( children: queries.asMap().entries.map((entry) { final index = entry.key; final state = entry.value; final userId = userIds[index];
return UserCard( userId: userId, state: state, ); }).toList(), ); }}Error Handling
Section titled “Error Handling”class ErrorHandlingDashboard extends HookWidget { @override Widget build(BuildContext context) { final queries = useQueries([ QueryConfig('users', () => api.fetchUsers()), QueryConfig('posts', () => api.fetchPosts()), QueryConfig('comments', () => api.fetchComments()), ]);
final hasError = queries.any((q) => q.hasError); final errorQueries = queries.where((q) => q.hasError).toList();
return Column( children: [ if (hasError) ErrorBanner( errors: errorQueries.map((q) => q.error).toList(), onRetry: () { for (final query in errorQueries) { query.refetch(); } }, ),
// Show successful queries ...queries.where((q) => q.hasData).map((q) => DataWidget(state: q) ), ], ); }}Performance Tips
Section titled “Performance Tips”Stable Query Keys
Section titled “Stable Query Keys”// Good: Stable keysfinal queries = useQueries([ QueryConfig('users', () => api.fetchUsers()), QueryConfig('posts', () => api.fetchPosts()),]);
// Avoid: Dynamic keys that change on each renderfinal queries = useQueries([ QueryConfig('users-${DateTime.now()}', () => api.fetchUsers()), QueryConfig('posts-${DateTime.now()}', () => api.fetchPosts()),]);Memoized Configs
Section titled “Memoized Configs”class MemoizedDashboard extends HookWidget { @override Widget build(BuildContext context) { // Memoize configs to prevent unnecessary re-execution final configs = useMemoized(() => [ QueryConfig('users', () => api.fetchUsers()), QueryConfig('posts', () => api.fetchPosts()), ], []);
final queries = useQueries(configs);
return Column( children: [ UsersList(queries[0]), PostsList(queries[1]), ], ); }}Testing
Section titled “Testing”testWidgets('useQueries executes all queries', (tester) async { await tester.pumpWidget( HookBuilder( builder: (context) { final queries = useQueries([ QueryConfig('query1', () => Future.value('data1')), QueryConfig('query2', () => Future.value('data2')), ]);
return Column( children: queries.map((q) => Text(q.data?.toString() ?? 'loading') ).toList(), ); }, ), );
await tester.pumpAndSettle();
expect(find.text('data1'), findsOneWidget); expect(find.text('data2'), findsOneWidget);});Common Pitfalls
Section titled “Common Pitfalls”1. Changing Config Array Reference
Section titled “1. Changing Config Array Reference”// Bad: Creates new array on each renderWidget build(BuildContext context) { final queries = useQueries([ QueryConfig('users', () => api.fetchUsers()), QueryConfig('posts', () => api.fetchPosts()), ]);}
// Good: Stable array referenceclass Dashboard extends HookWidget { static const _configs = [ QueryConfig('users', () => api.fetchUsers()), QueryConfig('posts', () => api.fetchPosts()), ];
@override Widget build(BuildContext context) { final queries = useQueries(_configs); }}2. Not Handling Empty States
Section titled “2. Not Handling Empty States”// Bad: Assumes queries always existWidget build(BuildContext context) { final queries = useQueries(configs); return UsersList(queries[0]); // Could crash if configs is empty}
// Good: Handle empty statesWidget build(BuildContext context) { final queries = useQueries(configs); if (queries.isEmpty) return const SizedBox(); return UsersList(queries[0]);}3. Ignoring Loading States
Section titled “3. Ignoring Loading States”// Bad: No loading indicationWidget build(BuildContext context) { final queries = useQueries(configs); return Column( children: queries.map((q) => DataWidget(q)).toList(), );}
// Good: Show loading statesWidget build(BuildContext context) { final queries = useQueries(configs); return Column( children: [ if (queries.any((q) => q.isLoading)) LinearProgressIndicator(), ...queries.map((q) => DataWidget(q)), ], );}