Skip to content

Prefetching with Hooks

The Hooks adapter provides convenient hooks for prefetching queries in your React-like Flutter components.

The usePrefetchQuery hook returns a stable callback function for prefetching queries:

class UserCard extends HookWidget {
final String userId;
const UserCard({required this.userId});
@override
Widget build(BuildContext context) {
final prefetch = usePrefetchQuery<User>();
return Card(
child: InkWell(
onTap: () => Navigator.pushNamed(context, '/user/$userId'),
onHover: () => prefetch('user-$userId'.toQueryKey(), () => api.fetchUser(userId)),
child: Column(
children: [
Text('User $userId'),
Text('Hover to prefetch profile data'),
],
),
),
);
}
}
  • Stable Reference: The returned callback has a stable reference across renders
  • Type Safety: Full type safety with generic parameters
  • Error Handling: Prefetch errors are handled silently
  • Cache Respect: Automatically respects cache staleness

The usePrefetchOnMount hook prefetches queries when the component mounts:

class Dashboard extends HookWidget {
@override
Widget build(BuildContext context) {
// Prefetch dashboard data on mount
usePrefetchOnMount([
PrefetchConfig(queryKey: 'user-stats'.toQueryKey(), queryFn: () => api.fetchUserStats()),
PrefetchConfig(queryKey: 'recent-posts'.toQueryKey(), queryFn: () => api.fetchRecentPosts()),
PrefetchConfig(queryKey: 'notifications'.toQueryKey(), queryFn: () => api.fetchNotifications()),
]);
return DashboardContent();
}
}
  • Mount Trigger: Executes only when the component mounts
  • Parallel Execution: All queries are prefetched in parallel
  • Cleanup: Automatically handles cleanup when component unmounts
  • Dependency Tracking: Re-executes if the configs list changes

Prefetch based on conditions:

class UserProfile extends HookWidget {
final String userId;
final bool shouldPrefetchRelated;
@override
Widget build(BuildContext context) {
final prefetch = usePrefetchQuery();
useEffect(() {
if (shouldPrefetchRelated) {
prefetch('user-posts-$userId'.toQueryKey(), () => api.fetchUserPosts(userId));
prefetch('user-followers-$userId'.toQueryKey(), () => api.fetchUserFollowers(userId));
}
return null;
}, [userId, shouldPrefetchRelated]);
return ProfileContent();
}
}

Prefetch before navigation:

class UserList extends HookWidget {
@override
Widget build(BuildContext context) {
final prefetch = usePrefetchQuery<User>();
return ListView.builder(
itemBuilder: (context, index) {
final userId = users[index].id;
return ListTile(
title: Text(users[index].name),
onTap: () {
// Prefetch user details before navigation
prefetch('user-$userId'.toQueryKey(), () => api.fetchUser(userId));
Navigator.pushNamed(context, '/user/$userId');
},
);
},
);
}
}

Prefetch data for inactive tabs:

class TabbedInterface extends HookWidget {
@override
Widget build(BuildContext context) {
final prefetch = usePrefetchQuery();
return DefaultTabController(
length: 3,
child: Column(
children: [
TabBar(
onTap: (index) {
// Prefetch data for other tabs
switch (index) {
case 0:
prefetch('posts'.toQueryKey(), () => api.fetchPosts());
prefetch('comments'.toQueryKey(), () => api.fetchComments());
break;
case 1:
prefetch('users'.toQueryKey(), () => api.fetchUsers());
prefetch('comments'.toQueryKey(), () => api.fetchComments());
break;
case 2:
prefetch('users'.toQueryKey(), () => api.fetchUsers());
prefetch('posts'.toQueryKey(), () => api.fetchPosts());
break;
}
},
tabs: [
Tab(text: 'Users'),
Tab(text: 'Posts'),
Tab(text: 'Comments'),
],
),
Expanded(
child: TabBarView(
children: [
UsersTab(),
PostsTab(),
CommentsTab(),
],
),
),
],
),
);
}
}
class AppRouter {
static final router = GoRouter(
routes: [
GoRoute(
path: '/users',
builder: (context, state) {
// Prefetch user details on route mount
return HookBuilder(
builder: (context) {
usePrefetchOnMount([
PrefetchConfig(queryKey: 'users'.toQueryKey(), queryFn: () => api.fetchUsers()),
]);
return UsersPage();
},
);
},
routes: [
GoRoute(
path: '/:userId',
builder: (context, state) {
final userId = state.pathParameters['userId']!;
return HookBuilder(
builder: (context) {
usePrefetchOnMount([
PrefetchConfig(queryKey: 'user-$userId'.toQueryKey(), queryFn: () => api.fetchUser(userId)),
PrefetchConfig(queryKey: 'user-posts-$userId'.toQueryKey(), queryFn: () => api.fetchUserPosts(userId)),
]);
return UserProfilePage(userId: userId);
},
);
},
),
],
),
],
);
}

The usePrefetchQuery hook returns a stable reference, so you can safely use it in event handlers:

// Good: Stable reference
final prefetch = usePrefetchQuery();
onHover: () => prefetch('key'.toQueryKey(), fetchFn);
// Avoid: Creating new functions in render
onHover: () => usePrefetchQuery()('key', fetchFn); // Creates new function each render

Use usePrefetchOnMount for multiple related queries:

// Good: Batch prefetch
usePrefetchOnMount([
PrefetchConfig(queryKey: 'user'.toQueryKey(), queryFn: () => api.fetchUser()),
PrefetchConfig(queryKey: 'posts'.toQueryKey(), queryFn: () => api.fetchPosts()),
]);
// Less efficient: Individual prefetches
useEffect(() {
prefetch('user'.toQueryKey(), () => api.fetchUser());
prefetch('posts'.toQueryKey(), () => api.fetchPosts());
}, []);

Only prefetch when conditions are met:

useEffect(() {
if (isAuthenticated && hasPermission) {
prefetch('sensitive-data'.toQueryKey(), () => api.fetchSensitiveData());
}
}, [isAuthenticated, hasPermission]);

Test prefetching hooks:

testWidgets('usePrefetchQuery returns stable callback', (tester) async {
late void Function(String, Future<String> Function()) prefetch1;
late void Function(String, Future<String> Function()) prefetch2;
await tester.pumpWidget(
MaterialApp(
home: HookBuilder(
builder: (context) {
prefetch1 = usePrefetchQuery<String>();
prefetch2 = usePrefetchQuery<String>();
return SizedBox();
},
),
),
);
expect(prefetch1, equals(prefetch2));
});
testWidgets('usePrefetchOnMount prefetches on mount', (tester) async {
int fetchCount = 0;
Future<String> fetchData() async {
fetchCount++;
return 'test-data';
}
await tester.pumpWidget(
MaterialApp(
home: HookBuilder(
builder: (context) {
usePrefetchOnMount([
PrefetchConfig(queryKey: 'test-key'.toQueryKey(), queryFn: fetchData),
]);
return SizedBox();
},
),
),
);
await tester.pump();
expect(fetchCount, equals(1));
});

The Hooks adapter makes prefetching intuitive and React-like, providing a clean API for predictive data loading in your Flutter applications.