fasq_hooks 0.2.1
fasq_hooks: ^0.2.1 copied to clipboard
Flutter Hooks adapter for FASQ (Flutter Async State Query) - async state management with hooks
fasq_hooks #
Flutter Hooks adapter for FASQ (Flutter Async State Query) - bringing powerful async state management to your hooks-based Flutter apps.
Features #
- π£
useQuery- Declarative data fetching with hooks - βΎοΈ
useInfiniteQuery- Infinite queries for pagination and load-more - π
useMutation- Server mutations made simple - π
useQueries- Execute multiple queries in parallel - π Automatic caching - Built on FASQ's production-ready cache
- β‘ Background refetching - Stale-while-revalidate pattern
- π― Type-safe - Full TypeScript-like type safety
Installation #
dependencies:
fasq_hooks: ^0.1.0
Usage #
Infinite Queries #
final posts = useInfiniteQuery<List<Post>, int>(
'posts',
(page) => api.fetchPosts(page: page),
InfiniteQueryOptions(
getNextPageParam: (pages, last) => pages.length + 1,
),
);
if (posts.hasNextPage) {
// trigger load more
}
Parallel Queries #
Execute multiple queries in parallel using useQueries or useNamedQueries:
// Index-based access
final queries = useQueries([
QueryConfig('users', () => api.fetchUsers()),
QueryConfig('posts', () => api.fetchPosts()),
QueryConfig('comments', () => api.fetchComments()),
]);
// Named access (more ergonomic)
final queries = useNamedQueries({
'users': QueryConfig('users', () => api.fetchUsers()),
'posts': QueryConfig('posts', () => api.fetchPosts()),
'comments': QueryConfig('comments', () => api.fetchComments()),
});
// Access by name
final users = queries['users']?.data;
final posts = queries['posts']?.data;
Prefetching #
Warm the cache before data is needed:
// Prefetch on demand
final prefetch = usePrefetchQuery();
onHover: () => prefetch('user-123', () => api.fetchUser('123'));
// Prefetch on mount
usePrefetchOnMount([
PrefetchConfig(key: 'users', queryFn: () => api.fetchUsers()),
PrefetchConfig(key: 'posts', queryFn: () => api.fetchPosts()),
]);
final allLoaded = queries.every((q) => q.hasData);
final anyError = queries.any((q) => q.hasError);
return Column(
children: [
if (!allLoaded) LinearProgressIndicator(),
if (anyError) ErrorBanner(),
UsersList(queries[0]),
PostsList(queries[1]),
CommentsList(queries[2]),
],
);
// Named access (better DX)
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']!),
],
);
Dependent Queries #
final user = useQuery('user', () => fetchUser());
final posts2 = useQuery(
'posts:${user.data?.id}',
() => fetchPosts(user.data!.id),
options: QueryOptions(enabled: user.isSuccess),
);
Basic Query #
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(),
options: QueryOptions(
staleTime: Duration(minutes: 5),
),
);
if (usersState.isLoading) {
return CircularProgressIndicator();
}
if (usersState.hasError) {
return Text('Error: ${usersState.error}');
}
if (usersState.hasData) {
return UserList(users: usersState.data!);
}
return SizedBox();
}
}
Mutations #
class CreateUserScreen extends HookWidget {
@override
Widget build(BuildContext context) {
final createUser = useMutation<User, String>(
(name) => api.createUser(name),
onSuccess: (user) {
print('Created user: ${user.name}');
// Invalidate users query
useQueryClient().invalidateQuery('users');
},
);
return ElevatedButton(
onPressed: createUser.isLoading
? null
: () => createUser.mutate('John Doe'),
child: Text('Create User'),
);
}
}
Access QueryClient #
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'),
);
}
}
API Reference #
useQuery #
QueryState<T> useQuery<T>(
String key,
Future<T> Function() queryFn, {
QueryOptions? options,
})
Parameters:
key- Unique identifier for the queryqueryFn- Async function that fetches the dataoptions- Optional configuration (staleTime, cacheTime, etc.)
Returns: QueryState<T> with:
isLoading- Initial loading stateisFetching- Background refetch in progresshasData- Whether data is availabledata- The fetched datahasError- Whether an error occurrederror- The error object
useMutation #
MutationState<TData, TVariables> useMutation<TData, TVariables>(
Future<TData> Function(TVariables) mutationFn, {
void Function(TData)? onSuccess,
void Function(Object)? onError,
})
Parameters:
mutationFn- Function that performs the mutationonSuccess- Called when mutation succeedsonError- Called when mutation fails
Returns: MutationState<TData, TVariables> with:
mutate(variables)- Execute the mutationreset()- Reset mutation stateisLoading- Whether mutation is in progressdata- Mutation resulterror- Mutation error
useQueryClient #
QueryClient useQueryClient()
Returns the global QueryClient instance for manual cache management.
Security Features π #
fasq_hooks supports all FASQ security features through QueryClient configuration:
Secure Queries with useQuery #
class SecureDataWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final client = context.queryClient;
final secureQuery = useQuery<String>(
'auth-token',
() => api.getAuthToken(),
options: QueryOptions(
isSecure: true, // Mark as secure
maxAge: Duration(minutes: 15), // Required TTL
staleTime: Duration(minutes: 5),
),
client: client, // Use configured client
);
if (secureQuery.isLoading) return CircularProgressIndicator();
if (secureQuery.hasError) return Text('Error: ${secureQuery.error}');
// Secure data never persisted, cleared on app background
return Text('Token: ${secureQuery.data}');
}
}
Secure Mutations with useMutation #
class SecureMutationWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final client = context.queryClient;
final mutation = useMutation<String, String>(
mutationFn: (data) => api.secureMutation(data),
options: MutationOptions(
queueWhenOffline: true,
maxRetries: 3,
),
client: client, // Use configured client
);
return ElevatedButton(
onPressed: mutation.isLoading
? null
: () => mutation.mutate('secure-data'),
child: mutation.isLoading
? CircularProgressIndicator()
: Text('Secure Mutation'),
);
}
}
Global Security Configuration #
QueryClientProvider(
config: CacheConfig(
defaultStaleTime: Duration(minutes: 5),
defaultCacheTime: Duration(minutes: 10),
),
persistenceOptions: PersistenceOptions(
enabled: true,
encryptionKey: 'your-encryption-key',
),
child: MaterialApp(
home: MyApp(),
),
)
Security Benefits:
- β Secure cache entries with automatic cleanup
- β Encrypted persistence for sensitive data
- β Input validation preventing injection attacks
- β Platform-specific secure key storage
Why Hooks? #
If you're already using flutter_hooks, this adapter provides the most natural integration with Flutter Query. The hooks API is:
- Concise - Less boilerplate than StatefulWidget
- Composable - Easy to create custom hooks
- Familiar - Similar to React Query hooks
- Testable - Hooks are easy to test
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, so they have identical performance and caching behavior.
Learn More #
License #
MIT