This episode covers parallel queries with useQueries and query composition, as well as dependent queries that wait for another query's result via the enabled option. You learn the correct pattern for fetching interdependent data.

Real applications rarely fetch just one piece of data. A dashboard needs statistics, notifications, and an activity list all at once. A profile page needs the user's data and their posts — and the posts can only be fetched once the user is known. Episode 8 teaches two patterns for handling this complexity: parallel queries and dependent queries.
Parallel queries mean several queries run at the same time. The simplest way is to call multiple useQuery hooks at once in a single component:
function Dashboard() {
const stats = useQuery({ queryKey: ["stats"], queryFn: fetchStats })
const notifs = useQuery({ queryKey: ["notifs"], queryFn: fetchNotifs })
const activity = useQuery({ queryKey: ["activity"], queryFn: fetchActivity })
return <div>...</div>
}The three queries above run in parallel because there is no dependency between them. Each useQuery has its own cache and status — this approach is simple and sufficient for most cases.
There are times when the number of queries isn't known when writing the code — for example a query for each item in a list. This is where useQueries shines:
import { useQueries } from "@tanstack/react-query"
function TodoBatch({ ids }) {
const results = useQueries({
queries: ids.map((id) => ({
queryKey: ["todos", id],
queryFn: () => fetchTodo(id),
})),
})
const semuaSelesai = results.every((r) => r.isSuccess)
if (!semuaSelesai) return <p>Memuat...</p>
return <ul>{results.map((r) => <li key={r.data.id}>{r.data.title}</li>)}</ul>
}useQueries accepts an object with a queries array, and returns an array of results parallel to the input. ids.map produces a query configuration for each id, and all queries run in parallel. Each results[i] is a status object just like the one returned by useQuery.
A dependent query is a query that only runs after another query finishes, because its data is needed as a parameter. The control key is the enabled option:
function UserPosts({ userId }) {
const user = useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
enabled: !!userId,
})
const posts = useQuery({
queryKey: ["user", userId, "posts"],
queryFn: () => fetchUserPosts(userId),
enabled: !!user.data,
})
if (user.isLoading) return <p>Memuat user...</p>
if (posts.isLoading) return <p>Memuat post...</p>
return <div>{posts.data.map((p) => <p key={p.id}>{p.title}</p>)}</div>
}The posts query is set to enabled: !!user.data, so it waits for user.data to be available before running. enabled: !!userId on the first query ensures the query doesn't run before userId is valid — a pattern that prevents wasteful fetches.
Without enabled, a query still runs even if its parameters aren't available yet. As a result, queryFn receives undefined and makes a wrong request or throws an error. enabled gives you explicit control over when a query may start:
// salah: posts langsung jalan walau user belum ada
const posts = useQuery({
queryKey: ["posts", userId],
queryFn: fetchPosts,
})
// benar: posts menunggu user.data
const posts = useQuery({
queryKey: ["user", userId, "posts"],
queryFn: () => fetchUserPosts(userId),
enabled: !!user.data,
})The difference enabled: !!user.data makes the second query wait for the user data. A useQuery with enabled: false enters the pending status and adds no network load until enabled becomes true.
The enabled: !!userId pattern also acts as a guard: if userId is empty, the query doesn't run at all. This is useful for pages waiting on input — for example a dropdown that selects an ID before details are loaded.
Warning
Don't put fetch outside queryFn just because you want to wait for data. Always use enabled and let TanStack Query decide when queryFn is called — this keeps the query status, cache, and retry consistent.
For longer dependency chains — user → profile → settings — combine enabled in levels. Each level waits for the data of the level before it:
user (enabled: !!userId)
→ profile (enabled: !!user.data)
→ settings (enabled: !!profile.data)The user → profile → settings diagram illustrates the correct sequential flow. enabled accepts a boolean value from any data status, so chains of any length stay deterministic.
Episode 8 gave you two weapons for large and interdependent data: parallel queries with useQueries for dynamic lists, and dependent queries with enabled for correct sequential fetching.
Key takeaways:
useQuery calls or useQueries.useQueries fits dynamic lists of queries whose count varies.enabled stops a query until its condition is met.enabled: !!data.enabled also acts as a guard for empty parameters.enabled.In the next episode, episode 9, we will discuss pagination and infinite queries — useInfiniteQuery with getNextPageParam for a Load More button, and simple pagination with placeholderData and keepPreviousData for a smooth UX without flicker.