Hooks Adapter
The hooks adapter provides a declarative API for Fasq using flutter_hooks. Perfect for developers who prefer React-like hooks patterns.
Overview
Section titled “Overview”The hooks adapter provides:
useQuery- Hook for executing queriesuseMutation- Hook for executing mutationsuseQueryClient- Hook for accessing QueryClient- Custom hooks - Easy to create reusable query logic
When to Use Hooks Adapter
Section titled “When to Use Hooks Adapter”Use the hooks adapter when:
- You’re already using
flutter_hooks - You prefer declarative APIs
- You want React-like patterns
- You like composable logic
Installation
Section titled “Installation”Add the adapter and Hooks to your project:
flutter pub add fasq_hooks flutter_hooks[!NOTE] This adapter is built on top of the fasq core package, which will be added automatically as a dependency.
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', () => api.fetchUsers());
if (usersState.isLoading) { return CircularProgressIndicator(); }
if (usersState.hasError) { return Text('Error: ${usersState.error}'); }
if (usersState.hasData) { return ListView.builder( itemCount: usersState.data!.length, itemBuilder: (context, index) { final user = usersState.data![index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ); }
return SizedBox(); }}Key Features
Section titled “Key Features”Declarative API
Section titled “Declarative API”Hooks provide a clean, declarative way to manage queries:
class UserProfile extends HookWidget { final String userId;
const UserProfile({required this.userId});
@override Widget build(BuildContext context) { final userState = useQuery( 'user:$userId', () => api.fetchUser(userId), options: QueryOptions( staleTime: Duration(minutes: 5), ), );
if (userState.isLoading) return CircularProgressIndicator(); if (userState.hasError) return Text('Error: ${userState.error}'); if (userState.hasData) return UserDetails(userState.data!);
return SizedBox(); }}Automatic Dependency Tracking
Section titled “Automatic Dependency Tracking”Hooks automatically track dependencies and only refetch when they change:
class UserPosts extends HookWidget { final String userId;
const UserPosts({required this.userId});
@override Widget build(BuildContext context) { // This will automatically refetch when userId changes final postsState = useQuery( 'posts:user:$userId', () => api.fetchUserPosts(userId), );
return buildUI(postsState); }}Composable Logic
Section titled “Composable Logic”Create custom hooks for reusable query logic:
// Custom hookQueryState<User> useUser(String userId) { return useQuery( 'user:$userId', () => api.fetchUser(userId), options: QueryOptions( staleTime: Duration(minutes: 5), ), );}
// Use the custom hookclass UserProfile extends HookWidget { final String userId;
const UserProfile({required this.userId});
@override Widget build(BuildContext context) { final userState = useUser(userId);
if (userState.isLoading) return CircularProgressIndicator(); if (userState.hasData) return UserDetails(userState.data!);
return SizedBox(); }}Mutations
Section titled “Mutations”Use useMutation for creating, updating, or deleting data:
class CreateUserScreen extends HookWidget { @override Widget build(BuildContext context) { final createUser = useMutation<User, String>( (name) => api.createUser(name), options: MutationOptions( onSuccess: (user) { print('Created user: ${user.name}'); // Invalidate users query useQueryClient().invalidateQuery('users'); }, onError: (error) { print('Error: $error'); }, ), );
return Column( children: [ ElevatedButton( onPressed: createUser.isLoading ? null : () => createUser.mutate('John Doe'), child: createUser.isLoading ? CircularProgressIndicator() : Text('Create User'), ), if (createUser.hasError) Text('Error: ${createUser.error}'), if (createUser.hasData) Text('Created: ${createUser.data!.name}'), ], ); }}QueryClient Access
Section titled “QueryClient Access”Access the QueryClient for manual operations:
class MyWidget extends HookWidget { @override Widget build(BuildContext context) { final queryClient = useQueryClient();
return ElevatedButton( onPressed: () { // Invalidate specific query queryClient.invalidateQuery('users');
// Set query data manually queryClient.setQueryData('user:1', User(id: 1, name: 'John'));
// Get cache info final info = queryClient.getCacheInfo(); print('Cache hit rate: ${info.metrics.hitRate}'); }, child: Text('Manage Cache'), ); }}Advanced Patterns
Section titled “Advanced Patterns”Conditional Queries
Section titled “Conditional Queries”class ConditionalQuery extends HookWidget { final String? userId;
const ConditionalQuery({this.userId});
@override Widget build(BuildContext context) { final userState = useQuery( 'user:$userId', () => 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(); }}Dependent Queries
Section titled “Dependent 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', () => api.fetchUser(userId), );
// Then fetch posts when user is available final postsState = useQuery( 'posts:user:$userId', () => 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(); }}Form Handling
Section titled “Form Handling”class CreateUserForm extends HookWidget { @override Widget build(BuildContext context) { final nameController = useTextEditingController(); final emailController = useTextEditingController();
final createUser = useMutation<User, Map<String, String>>( (data) => api.createUser(data), options: MutationOptions( onSuccess: (user) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('User created: ${user.name}')), ); nameController.clear(); emailController.clear(); }, ), );
return Column( children: [ TextField( controller: nameController, decoration: InputDecoration(labelText: 'Name'), ), TextField( controller: emailController, decoration: InputDecoration(labelText: 'Email'), ), ElevatedButton( onPressed: createUser.isLoading ? null : () { createUser.mutate({ 'name': nameController.text, 'email': emailController.text, }); }, child: createUser.isLoading ? CircularProgressIndicator() : Text('Create User'), ), ], ); }}Custom Hooks
Section titled “Custom Hooks”Create reusable query logic with custom hooks:
// Custom hook for user dataQueryState<User> useUser(String userId) { return useQuery( 'user:$userId', () => api.fetchUser(userId), options: QueryOptions( staleTime: Duration(minutes: 5), ), );}
// Custom hook for user postsQueryState<List<Post>> useUserPosts(String userId) { return useQuery( 'posts:user:$userId', () => api.fetchUserPosts(userId), options: QueryOptions( enabled: userId.isNotEmpty, ), );}
// Custom hook for creating usersMutationState<User, String> useCreateUser() { return useMutation<User, String>( (name) => api.createUser(name), options: MutationOptions( onSuccess: (user) { useQueryClient().invalidateQuery('users'); }, ), );}
// Use custom hooksclass UserDashboard extends HookWidget { final String userId;
const UserDashboard({required this.userId});
@override Widget build(BuildContext context) { final userState = useUser(userId); final postsState = useUserPosts(userId); final createPost = useCreateUser();
return Column( children: [ if (userState.hasData) UserHeader(userState.data!), if (postsState.hasData) PostsList(postsState.data!), ElevatedButton( onPressed: () => createPost.mutate('New Post'), child: Text('Create Post'), ), ], ); }}Type Safety
Section titled “Type Safety”Full generic type support ensures compile-time safety:
class UserScreen extends HookWidget { @override Widget build(BuildContext context) { // usersState.data is List<User>? final usersState = useQuery<List<User>>('users', () => api.fetchUsers());
// createUser.mutate expects String final createUser = useMutation<User, String>( (name) => api.createUser(name), );
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 Benefits
Section titled “Performance Benefits”- Automatic dependency tracking - Only refetches when dependencies change
- Composable logic - Reusable custom hooks
- Memory efficient - Automatic cleanup on unmount
- Type safe - Full generic type support
Comparison with Core Package
Section titled “Comparison with Core Package”Core Package (QueryBuilder):
QueryBuilder<List<User>>( queryKey: 'users', queryFn: () => api.fetchUsers(), builder: (context, state) { if (state.isLoading) return Loading(); return UserList(state.data!); },)Hooks Adapter (useQuery):
final usersState = useQuery('users', () => api.fetchUsers());if (usersState.isLoading) return Loading();return UserList(usersState.data!);Both approaches use the same underlying query engine and have identical performance.
Next Steps
Section titled “Next Steps”- useQuery - Deep dive into useQuery hook
- useMutation - Learn about mutations
- useQueryClient - Cache management
- Custom Hooks - Creating reusable logic
- Examples - Complete working examples