| | |

TanStack Query 12 ⚛️ Dependent Queries — Sequential query execution

⏱️ Estimated read: 14 min | |

A dependent query is a query whose execution depends on a value that is produced by another query or by a state outside the query. The previous chapters covered pagination and infinite queries, which fetch different slices of the same resource. This chapter covers the case where the second query cannot run until the first one has produced the value it needs—the user ID from a session query, the organization ID from a profile query, or the order ID from a list query. The enabled option is the mechanism that defers the query until the dependency is available, and the query key is the mechanism that ties the query to the value it depends on .

The problem that dependent queries solve is the wasted request. Without the enabled option, a query that needs a user ID runs immediately with undefined as the ID, and the request fails, or the component crashes when it tries to use the result. With enabled: !!userId, the query is registered in the cache but does not fetch until the ID is present. The isPending state is true while the query is disabled, so the component can render the loading state without distinguishing between “the query is running” and “the query is waiting for the dependency” . The fetchStatus property, however, is 'idle' while the query is disabled, which distinguishes it from a query that is actually fetching .

Key point: The enabled option does not remove the query from the cache. The query is still created, still has a key, and still subscribes to the store. It simply does not fetch until the condition is true. The isPending flag is true while the query is disabled, and the fetchStatus is 'idle'. The query key must include the value that the query depends on, so that the cache entry is distinct for each value of the dependency and the query refetches when the value changes .


Why dependent queries require the enabled option

The undefined-parameter problem.

A query that fetches '/api/users/' + userId cannot run until userId is defined. If the query runs with userId undefined, the request is sent to '/api/users/undefined', which either fails or returns the wrong data. The enabled: !!userId condition prevents the fetch until the value is available, and the component renders the loading state while isPending is true .

The chained-query problem.

A component often needs two queries in sequence: the first fetches the user’s profile, and the second fetches the user’s orders using the ID from the profile. The second query’s enabled option references the first query’s data. The second query does not fetch until the first query has resolved with the ID, and the component renders the loading state for the second query while the first is still running . The two queries have separate cache entries and separate states, but the second one is gated on the first.

The stale-dependency problem.

The query key must include the dependency value. If the query key is ['orders'] for every user, the cache entry is shared across users, and the second user sees the first user’s orders. The key ['orders', userId] creates a separate entry for each user, and the query refetches when userId changes . The enabled option controls when the query runs, and the query key controls which cache entry it uses; both must be correct for the dependent query to work.


a. The basic dependent query

The enabled option accepts a boolean or a function that returns a boolean. The simplest form is a boolean expression that checks whether the dependency is defined.

import { useQuery } from '@tanstack/react-query'

function UserProfile({ userId }: { userId: number | undefined }) {
  const { data, isPending, fetchStatus } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId!),
    enabled: !!userId,
  })

  if (isPending) {
    // fetchStatus is 'idle' while the query is disabled
    return <p>{fetchStatus === 'idle' ? 'Waiting for user ID...' : 'Loading...'}</p>
  }

  return <p>{data.name}</p>
}

The enabled: !!userId condition prevents the fetch until userId is defined. The query key includes userId, so the cache entry is distinct for each user . The isPending flag is true while the query is disabled, and the fetchStatus is 'idle', which the component uses to distinguish the waiting state from the loading state. The non-null assertion userId! in the query function is safe because the function only runs when enabled is true, but it is better to write the function so that it does not need the assertion.

const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: async () => {
    if (!userId) throw new Error('userId is required')
    return fetchUser(userId)
  },
  enabled: !!userId,
})

The explicit check inside the function makes the requirement visible and avoids the non-null assertion. The function throws if it is called without the ID, which should never happen because the enabled condition prevents it, but the check documents the dependency .


b. The chained-query pattern

The chained-query pattern runs one query after another. The first query fetches the profile, and the second query fetches the orders using the profile’s ID. The second query’s enabled option references the first query’s data.

function UserOrders({ userId }: { userId: number }) {
  const profileQuery = useQuery({
    queryKey: ['profile', userId],
    queryFn: () => fetchProfile(userId),
    enabled: !!userId,
  })

  const organizationId = profileQuery.data?.organizationId

  const ordersQuery = useQuery({
    queryKey: ['orders', organizationId],
    queryFn: () => fetchOrders(organizationId!),
    enabled: !!organizationId,
  })

  if (profileQuery.isPending) return <p>Loading profile...</p>
  if (profileQuery.isError) return <p>Profile error</p>

  if (ordersQuery.isPending) return <p>Loading orders...</p>
  if (ordersQuery.isError) return <p>Orders error</p>

  return (
    <ul>
      {ordersQuery.data.map((order) => (
        <li key={order.id}>{order.title}</li>
      ))}
    </ul>
  )
}

The profileQuery runs first, and its data.organizationId is the dependency for the ordersQuery. The ordersQuery does not run until organizationId is defined, and the component renders the profile loading state, then the orders loading state, then the data . The two queries are independent cache entries, and each has its own loading and error state.

The component can also combine the loading states with useQueries or with a single loading check, but the separate checks make the sequence visible. The isPending of the second query is true while the first query is still running, because the second query’s enabled condition is false and its fetchStatus is 'idle' . The distinction between the two loading states is useful for the UI: the profile loading state and the orders loading state can have different placeholders.


c. The enabled function and the fetchStatus distinction

The enabled option can be a function that receives the query and returns a boolean. The function form is useful when the condition depends on the query’s own state or on multiple values.

const { data } = useQuery({
  queryKey: ['search', query, filters],
  queryFn: () => searchApi(query, filters),
  enabled: (query) => {
    // Only search if the query is at least 3 characters and the previous search succeeded
    return query.queryKey[1].length >= 3 && !query.state.error
  },
})

The function receives the query object, which contains the queryKey, the state, and other properties. The condition can reference any of them . The function form is rarely needed, but it is the escape hatch for complex conditions that cannot be expressed as a boolean expression.

The fetchStatus property distinguishes the disabled state from the fetching state. The isPending flag is true in both cases, but the fetchStatus is 'idle' when the query is disabled and 'fetching' when it is running.

if (isPending) {
  if (fetchStatus === 'idle') {
    return <p>Waiting for data...</p>
  }
  return <p>Loading...</p>
}

The fetchStatus === 'idle' check is the way to render a different message for the waiting state. Without it, the component cannot distinguish between “the query is disabled and waiting for the dependency” and “the query is fetching” . The distinction is useful for the UI, because the waiting state is usually a placeholder that does not need a spinner.


Complete Example Session

// ============================================
// PART 1: THE FIRST QUERY
// ============================================
import { useQuery } from '@tanstack/react-query'

interface Profile {
  id: number
  name: string
  organizationId: number
}

function useProfile(userId: number) {
  return useQuery({
    queryKey: ['profile', userId],
    queryFn: async () => {
      const res = await fetch(`/api/users/${userId}/profile`)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      return res.json() as Promise<Profile>
    },
    enabled: !!userId,
  })
}

// ============================================
// PART 2: THE DEPENDENT QUERY
// ============================================
interface Order {
  id: number
  title: string
}

function useOrders(organizationId: number | undefined) {
  return useQuery({
    queryKey: ['orders', organizationId],
    queryFn: async () => {
      if (!organizationId) throw new Error('organizationId is required')
      const res = await fetch(`/api/organizations/${organizationId}/orders`)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      return res.json() as Promise<Order[]>
    },
    enabled: !!organizationId,
  })
}

// ============================================
// PART 3: THE COMPONENT
// ============================================
function UserOrders({ userId }: { userId: number }) {
  const profile = useProfile(userId)
  const orders = useOrders(profile.data?.organizationId)

// ============================================
// PART 4: THE PROFILE STATES
// ============================================
  if (profile.isPending) {
    return <p>{profile.fetchStatus === 'idle' ? 'Waiting...' : 'Loading profile...'}</p>
  }
  if (profile.isError) {
    return <p>Profile error: {profile.error.message}</p>
  }

// ============================================
// PART 5: THE ORDERS STATES
// ============================================
  if (orders.isPending) {
    return <p>{orders.fetchStatus === 'idle' ? 'Waiting for organization...' : 'Loading orders...'}</p>
  }
  if (orders.isError) {
    return <p>Orders error: {orders.error.message}</p>
  }

// ============================================
// PART 6: THE RENDER
// ============================================
  return (
    <div>
      <h1>{profile.data.name}</h1>
      <ul>
        {orders.data.map((order) => (
          <li key={order.id}>{order.title}</li>
        ))}
      </ul>
    </div>
  )
}

The six parts demonstrate the dependent query: the first query, the dependent query, the component that composes them, the profile states, the orders states, and the render. The critical configuration is enabled: !!organizationId in the second query, which prevents the fetch until the first query has produced the ID.


Quick Reference

enabled Option

FormExampleUse Case
Booleanenabled: !!userIdSimple dependency check
Booleanenabled: userId > 0Numeric condition
Functionenabled: (query) => query.queryKey[1].length >= 3Complex condition

State Flags

FlagDisabled QueryFetching Query
isPendingtruetrue
fetchStatus'idle''fetching'
isFetchingfalsetrue
dataundefinedundefined

Chained Query Pattern

StepCode
First queryuseQuery({ queryKey: ['profile', userId], ... })
Extract dependencyconst orgId = profile.data?.organizationId
Second queryuseQuery({ queryKey: ['orders', orgId], enabled: !!orgId, ... })

Best Practices

✅ Do This:

// Use enabled with the dependency check
enabled: !!userId                                              # ✅

// Include the dependency in the query key
queryKey: ['user', userId]                                     # ✅

// Check the dependency inside the query function
queryFn: async () => { if (!userId) throw new Error('...'); ... } # ✅

// Distinguish waiting from loading with fetchStatus
{isPending && fetchStatus === 'idle' ? 'Waiting' : 'Loading'}  # ✅

// Chain the queries with the data from the previous query
const orgId = profile.data?.organizationId                     # ✅

// Render the first query's loading state before the second query's
if (profile.isPending) return <ProfileSkeleton />              # ✅

❌ Don’t Do This:

// Run the query without the dependency
useQuery({ queryKey: ['user'], queryFn: () => fetchUser(undefined) }) # ❌

// Omit the dependency from the query key
queryKey: ['orders']    // Shared cache entry across users     # ❌

// Use the non-null assertion without the enabled check
queryFn: () => fetchUser(userId!)    // Fails if userId is undefined # ❌

// Treat the disabled query as an error
if (isPending) return <Error />    // The query is not fetching    # ❌

// Forget the second query's loading state
return <Orders data={orders.data} />    // Crashes when data is undefined # ❌

Common Pitfalls

PitfallWhy It HappensFix
Query fetches with undefinedNo enabled conditionAdd enabled: !!dependency
Wrong user’s dataDependency omitted from the keyInclude it in the key
Loading state never resolvesDependency never becomes definedCheck the first query
isPending is true but no fetchThe query is disabledCheck fetchStatus === 'idle'
Second query runs too earlyenabled references the wrong valueUse the first query’s data
Component crashesSecond query’s data is undefinedRender the loading state first

Real-World Examples

1. Basic dependent query

useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), enabled: !!userId })

2. Chained queries

const profile = useQuery({ queryKey: ['profile', userId], queryFn: () => fetchProfile(userId) })
const orders = useQuery({ queryKey: ['orders', profile.data?.orgId], queryFn: () => fetchOrders(profile.data!.orgId), enabled: !!profile.data?.orgId })

3. Search with minimum length

useQuery({ queryKey: ['search', query], queryFn: () => search(query), enabled: query.length >= 3 })

4. Enabled function

enabled: (q) => q.queryKey[1].length >= 3

5. Waiting vs loading

if (isPending) return fetchStatus === 'idle' ? <Waiting /> : <Spinner />

6. Dependency from props

useQuery({ queryKey: ['post', postId], queryFn: () => fetchPost(postId), enabled: !!postId })

7. Dependency from state

useQuery({ queryKey: ['results', filters], queryFn: () => fetchResults(filters), enabled: !!filters })

8. Dependency from another query

const orgId = profile.data?.organizationId
useQuery({ queryKey: ['org', orgId], queryFn: () => fetchOrg(orgId), enabled: !!orgId })

9. Multiple dependencies

enabled: !!userId && !!orgId

10. Disable a query

enabled: false

Visual

Chained Query Flow

┌──────────────────────────────────────────────────────────────┐
│  THE SECOND QUERY WAITS FOR THE FIRST QUERY'S DATA           │
│                                                              │
│  Component mounts with userId = 5                            │
│       │                                                      │
│       ▼                                                      │
│  profileQuery: enabled: !!5 → true                           │
│       │ fetch /api/users/5/profile                           │
│       ▼                                                      │
│  profileQuery.data = { organizationId: 42 }                  │
│       │                                                      │
│       ▼                                                      │
│  ordersQuery: enabled: !!42 → true                           │
│       │ fetch /api/organizations/42/orders                   │
│       ▼                                                      │
│  ordersQuery.data = [...]                                    │
│       │                                                      │
│       ▼                                                      │
│  Component renders the profile and the orders                │
└──────────────────────────────────────────────────────────────┘

Disabled vs Fetching

┌──────────────────────────────────────────────────────────────┐
│  isPending IS TRUE IN BOTH STATES, BUT fetchStatus DIFFERS   │
│                                                              │
│  Disabled query:                                             │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ enabled: false                                         │  │
│  │ isPending: true                                        │  │
│  │ fetchStatus: 'idle'                                    │  │
│  │ isFetching: false                                      │  │
│  │ → render "Waiting for dependency"                      │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Fetching query:                                             │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ enabled: true                                          │  │
│  │ isPending: true                                        │  │
│  │ fetchStatus: 'fetching'                                │  │
│  │ isFetching: true                                       │  │
│  │ → render "Loading"                                     │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

The Query Key with the Dependency

┌──────────────────────────────────────────────────────────────┐
│  THE KEY MUST INCLUDE THE DEPENDENCY                          │
│                                                              │
│  ❌ Incorrect:                                               │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ queryKey: ['orders']                                   │  │
│  │ → one cache entry for every user                       │  │
│  │ → user 2 sees user 1's orders                          │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  ✅ Correct:                                                 │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ queryKey: ['orders', organizationId]                   │  │
│  │ → a separate cache entry per organization              │  │
│  │ → the query refetches when the ID changes              │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Deferral optionenabled
Simple formenabled: !!dependency
Function formenabled: (query) => boolean
Disabled stateisPending: true, fetchStatus: 'idle'
Key requirementDependency in queryKey
Chained patternenabled: !!firstQuery.data?.id
Function guardCheck the dependency inside queryFn
Waiting vs loadingfetchStatus === 'idle'

Key takeaways:

  • The enabled option defers the query until the dependency is available. The query is registered in the cache and subscribes to the store, but it does not fetch until the condition is true. The isPending flag is true while the query is disabled, and the fetchStatus is 'idle' .
  • The query key must include the dependency. Without the dependency in the key, the cache entry is shared across different values of the dependency, and the component displays the wrong data. The key ['orders', organizationId] creates a separate entry for each organization and refetches when the ID changes .
  • The chained-query pattern uses the first query’s data as the second query’s dependency. The second query’s enabled option references firstQuery.data?.id, and the component renders the first query’s loading state before the second query’s. The two queries have separate cache entries and separate states.
  • fetchStatus distinguishes the waiting state from the fetching state. The isPending flag is true in both cases, but the fetchStatus is 'idle' when the query is disabled and 'fetching' when it is running. The distinction allows the component to render a different message or placeholder for the waiting state .
  • The query function should check the dependency explicitly. The enabled condition prevents the function from running without the dependency, but the explicit check documents the requirement and avoids the non-null assertion. The function throws if it is called without the dependency, which should never happen.
  • The component should render the first query’s loading state before the second query’s. The second query cannot run until the first query has produced the dependency, so the component must handle the sequence. The first query’s loading state is rendered while the first query is pending, and the second query’s loading state is rendered after the first query has resolved .

Remember: A dependent query is a query that cannot run until another value is available. The enabled option is the mechanism that defers the fetch, and the query key is the mechanism that ties the cache entry to the dependency. The isPending flag is true while the query is disabled, and the fetchStatus is 'idle', which distinguishes the waiting state from the loading state. The chained-query pattern uses the first query’s data as the second query’s dependency, and the component renders the loading states in sequence. The query function checks the dependency explicitly, and the query key includes it. The result is a sequence of queries that runs in the correct order, without wasted requests and without the wrong data.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!