qora_hooks
flutter_hooks integration for Qora: provides useQuery, useMutation, useInfiniteQuery, useFetchStatus, and useQueryClient.
Part of the Qora monorepo.
Why a separate package?
flutter_hooks is an optional dependency. Bundling hooks inside qora or qora_flutter would impose it on every user, even those who prefer QoraBuilder / QoraMutationBuilder. A separate package keeps things opt-in.
Getting started
Add both qora_flutter and qora_hooks to your pubspec.yaml:
dependencies:
qora_flutter: ^1.3.0
qora_hooks: ^1.3.0
flutter_hooks: ^0.21.2
Wrap your app with QoraScope:
void main() {
runApp(
QoraScope(
client: QoraClient(),
child: const MyApp(),
),
);
}
Usage
useQuery<T>
Fetches data on mount, caches it, and rebuilds the widget on every state change. Initializes from the cache synchronously; no loading flash when data is already fresh.
class UserScreen extends HookWidget {
final String userId;
const UserScreen({super.key, required this.userId});
@override
Widget build(BuildContext context) {
final state = useQuery<User>(
key: ['users', userId],
fetcher: () => Api.getUser(userId),
options: const QoraOptions(staleTime: Duration(minutes: 5)),
);
return switch (state) {
Initial() => const SizedBox.shrink(),
Loading() => const CircularProgressIndicator(),
Success(:final data) => UserCard(data),
Failure(:final error) => ErrorView(error),
};
}
}
useMutation<TData, TVariables>
TData is the type returned by the mutator. TVariables is the type passed to mutation.mutate(): typically a payload class or record.
class EditProfileScreen extends HookWidget {
@override
Widget build(BuildContext context) {
final mutation = useMutation<User, UpdateUserInput>(
mutator: (input) => Api.updateUser(input),
options: MutationOptions(
onSuccess: (user, _, __) async =>
QoraScope.of(context).invalidate(['users', user.id]),
),
);
return ElevatedButton(
onPressed: mutation.isPending
? null
: () => mutation.mutate(UpdateUserInput(name: 'Alice')),
child: mutation.isPending
? const CircularProgressIndicator()
: const Text('Save'),
);
}
}
useInfiniteQuery<TData, TPageParam>
final query = useInfiniteQuery<PostsPage, String?>(
key: const ['posts'],
fetcher: (cursor) => Api.getPosts(cursor: cursor),
getNextPageParam: (page) => page.nextCursor,
initialPageParam: null,
);
final allPosts = query.pages.expand((p) => p.posts).toList();
Call query.fetchNextPage() when the user reaches the end of the list to load the next page. query.hasNextPage becomes false when getNextPageParam returns null.
Rebuild only when derived data changes
select is a rebuild filter: the widget rebuilds when the state kind or the selected value changes. The returned state is always the latest one.
final state = useQuery<List<User>>(
key: ['users'],
fetcher: () => api.getUsers(),
select: (users) => users.length,
);
final count = state.dataOrNull?.length ?? 0;
useFetchStatus
Observes the engine status of a query without fetching: fetching, paused (offline) or idle.
final status = useFetchStatus(['users']);
if (status == FetchStatus.paused) return const OfflineBanner();
useQueryClient
final client = useQueryClient(); // nearest QoraClient from QoraScope
Additional information
- Source: github.com/meragix/qora
- Issues: github.com/meragix/qora/issues
- Documentation: qora.meragix.dev
Libraries
- qora_hooks
- flutter_hooks integration for Qora.