qora_hooks

pub.dev CI likes pub points

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

Libraries

qora_hooks
flutter_hooks integration for Qora.