PatternsData Fetching Strategies
Parallel and Dependent Queries
Efficiently fetch multiple related data sources
Optimize data fetching by running queries in parallel or sequentially when needed. This guide covers both patterns.
🎯 The Problem
// ❌ BAD: Sequential (waterfall)
const user = await fetchUser();
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
// Total time: 600ms + 400ms + 300ms = 1300ms
// ✅ GOOD: Parallel (when possible)
const [user, categories, settings] = await Promise.all([
fetchUser(),
fetchCategories(),
fetchSettings()
]);
// Total time: max(600ms, 200ms, 150ms) = 600ms⚡ Pattern 1: Parallel Queries
Fetch independent data simultaneously.
import { useQueries } from '@tanstack/react-query';
function Dashboard() {
const results = useQueries({
queries: [
{
queryKey: ['user'],
queryFn: fetchUser
},
{
queryKey: ['stats'],
queryFn: fetchStats
},
{
queryKey: ['notifications'],
queryFn: fetchNotifications
},
{
queryKey: ['settings'],
queryFn: fetchSettings
}
]
});
// All queries run in parallel!
const [userQuery, statsQuery, notificationsQuery, settingsQuery] = results;
// Check loading state
const isLoading = results.some(query => query.isLoading);
if (isLoading) return <Spinner />;
return (
<div>
<UserProfile user={userQuery.data} />
<Stats data={statsQuery.data} />
<Notifications items={notificationsQuery.data} />
<Settings data={settingsQuery.data} />
</div>
);
}With Individual Hooks
function Dashboard() {
// All three queries run in parallel
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
const { data: stats } = useQuery({
queryKey: ['stats'],
queryFn: fetchStats
});
const { data: notifications } = useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications
});
return (
<div>
<UserProfile user={user} />
<Stats data={stats} />
<Notifications items={notifications} />
</div>
);
}Pros:
- ✅ Fastest possible loading
- ✅ Simple to implement
- ✅ Independent failures
Cons:
- ⚠️ Higher initial network usage
- ⚠️ May fetch unnecessary data
🔗 Pattern 2: Dependent Queries (Waterfall)
Fetch data that depends on previous data.
function UserPosts({ userId }: { userId: string }) {
// First, fetch user
const { data: user, isLoading: userLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// Then fetch user's posts (depends on user)
const { data: posts, isLoading: postsLoading } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user!.id),
enabled: !!user, // Only run when user exists
});
if (userLoading) return <Spinner />;
if (!user) return <div>User not found</div>;
if (postsLoading) return <Spinner />;
return (
<div>
<h2>{user.name}'s Posts</h2>
{posts?.map(post => (
<Post key={post.id} post={post} />
))}
</div>
);
}Multi-Level Dependencies
function CommentThread({ postId }: { postId: string }) {
// Level 1: Fetch post
const { data: post } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId)
});
// Level 2: Fetch author (depends on post)
const { data: author } = useQuery({
queryKey: ['user', post?.authorId],
queryFn: () => fetchUser(post!.authorId),
enabled: !!post?.authorId
});
// Level 3: Fetch comments (depends on post)
const { data: comments } = useQuery({
queryKey: ['comments', postId],
queryFn: () => fetchComments(postId),
enabled: !!post
});
// Level 4: Fetch comment authors (depends on comments)
const commentAuthors = useQueries({
queries: (comments || []).map(comment => ({
queryKey: ['user', comment.authorId],
queryFn: () => fetchUser(comment.authorId)
}))
});
return (
<div>
<Post post={post} author={author} />
<Comments comments={comments} authors={commentAuthors} />
</div>
);
}Pros:
- ✅ Correct data dependencies
- ✅ Doesn't fetch unnecessary data
- ✅ Clear data flow
Cons:
- ⚠️ Slower (waterfall effect)
- ⚠️ Can't parallelize
🎯 Pattern 3: Optimistic Dependencies
Start dependent query early with estimated data.
function UserProfile({ username }: { username: string }) {
// Fetch user by username
const { data: user } = useQuery({
queryKey: ['user', username],
queryFn: () => fetchUserByUsername(username)
});
// Start fetching posts immediately (optimistic)
// Even though we don't have userId yet
const { data: posts } = useQuery({
queryKey: ['posts', username], // Use username as key
queryFn: async () => {
// Wait for user if not ready
if (!user) {
// Fetch user inline (will use cache)
const u = await fetchUserByUsername(username);
return fetchPosts(u.id);
}
return fetchPosts(user.id);
}
});
return (
<div>
<UserInfo user={user} />
<Posts posts={posts} />
</div>
);
}🚀 Pattern 4: Parallel + Dependent (Mixed)
Combine parallel and sequential fetching.
function Dashboard() {
// Step 1: Fetch user (required for everything)
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
// Step 2: Once user loaded, fetch multiple things in parallel
const results = useQueries({
queries: [
{
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user!.id),
enabled: !!user
},
{
queryKey: ['followers', user?.id],
queryFn: () => fetchFollowers(user!.id),
enabled: !!user
},
{
queryKey: ['following', user?.id],
queryFn: () => fetchFollowing(user!.id),
enabled: !!user
}
]
});
const [postsQuery, followersQuery, followingQuery] = results;
return (
<div>
<UserHeader user={user} />
<Posts posts={postsQuery.data} />
<Followers data={followersQuery.data} />
<Following data={followingQuery.data} />
</div>
);
}📦 Pattern 5: Batched Queries
Batch multiple queries into one request.
// Frontend
async function batchFetch(queries: Array<{ key: string; params: any }>) {
const response = await fetch('/api/batch', {
method: 'POST',
body: JSON.stringify({ queries })
});
return response.json();
}
// Usage
function Dashboard() {
const { data } = useQuery({
queryKey: ['dashboard-batch'],
queryFn: () => batchFetch([
{ key: 'user', params: {} },
{ key: 'posts', params: { limit: 10 } },
{ key: 'stats', params: {} }
])
});
return (
<div>
<UserProfile user={data?.user} />
<Posts posts={data?.posts} />
<Stats data={data?.stats} />
</div>
);
}
// Backend
app.post('/api/batch', async (req, res) => {
const { queries } = req.body;
const results = await Promise.all(
queries.map(async (query) => {
switch (query.key) {
case 'user':
return { key: 'user', data: await fetchUser() };
case 'posts':
return { key: 'posts', data: await fetchPosts(query.params) };
case 'stats':
return { key: 'stats', data: await fetchStats() };
default:
throw new Error(`Unknown query: ${query.key}`);
}
})
);
// Convert array to object
const data = results.reduce((acc, result) => {
acc[result.key] = result.data;
return acc;
}, {});
res.json(data);
});🔄 Pattern 6: GraphQL (Automatic Batching)
Let GraphQL handle batching and dependencies.
import { useQuery, gql } from '@apollo/client';
const DASHBOARD_QUERY = gql`
query Dashboard {
user {
id
name
email
}
posts(limit: 10) {
id
title
author {
name
}
}
stats {
views
likes
}
}
`;
function Dashboard() {
const { data, loading } = useQuery(DASHBOARD_QUERY);
if (loading) return <Spinner />;
return (
<div>
<UserProfile user={data.user} />
<Posts posts={data.posts} />
<Stats data={data.stats} />
</div>
);
}⚙️ Pattern 7: Smart Prefetching
Prefetch dependent data proactively.
function PostList() {
const queryClient = useQueryClient();
const { data: posts } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
onSuccess: (posts) => {
// Prefetch author data for all posts
posts.forEach(post => {
queryClient.prefetchQuery({
queryKey: ['user', post.authorId],
queryFn: () => fetchUser(post.authorId)
});
});
}
});
return (
<div>
{posts?.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
function PostCard({ post }: { post: Post }) {
// Author already prefetched, instant load!
const { data: author } = useQuery({
queryKey: ['user', post.authorId],
queryFn: () => fetchUser(post.authorId)
});
return (
<div>
<h3>{post.title}</h3>
<p>By {author?.name}</p>
</div>
);
}📊 Comparison Table
| Pattern | Speed | Complexity | Best For |
|---|---|---|---|
| Parallel | ⚡⚡⚡⚡⚡ | Low | Independent data |
| Dependent | ⚡⚡ | Low | Required sequence |
| Optimistic | ⚡⚡⚡⚡ | Medium | Predictable deps |
| Mixed | ⚡⚡⚡⚡ | Medium | Real-world apps |
| Batched | ⚡⚡⚡⚡ | High | Many small requests |
| GraphQL | ⚡⚡⚡⚡ | Medium | Complex relationships |
| Prefetch | ⚡⚡⚡⚡⚡ | Medium | Known patterns |
💡 Best Practices
1. Identify Dependencies
// ✅ GOOD: Clear dependencies
const user = await fetchUser();
const posts = await fetchPosts(user.id); // Depends on user
// ❌ BAD: Hidden dependencies
const posts = await fetchPosts(); // Which user?2. Minimize Waterfalls
// ❌ BAD: 3-level waterfall
const user = await fetchUser();
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
// 600ms + 400ms + 300ms = 1300ms
// ✅ GOOD: Parallel when possible
const user = await fetchUser();
const [posts, followers] = await Promise.all([
fetchPosts(user.id),
fetchFollowers(user.id)
]);
// 600ms + max(400ms, 200ms) = 1000ms3. Use enabled for Dependencies
// ✅ GOOD: Only fetch when ready
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
enabled: !!userId
});
// ❌ BAD: Fetch always
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId) // Error if userId is null!
});🏢 Real-World Examples
GitHub
// Parallel: User info, repos, activity
// Dependent: Repo → Contributors → User details// Parallel: Timeline, trends, suggestions
// Dependent: Tweet → Author → Followers// Batched: Profile sections in one query
// Prefetch: Network connections on hover📚 Key Takeaways
- Parallel by default - Fetch independent data together
- Sequential when needed - Use
enabledfor dependencies - Prefetch predictable deps - Load before needed
- Batch small requests - Reduce HTTP overhead
- GraphQL for complex - Let server optimize
- Monitor waterfalls - Use React DevTools
- Test with slow network - Waterfalls hurt more on 3G
Optimize for the critical path first, then parallelize everything else.