Skip to main content
Dependent queries (also known as serial queries) wait for previous queries to complete before executing. This is useful when you need data from one query to construct the next query.

Basic Dependent Query

Use the enabled option to control when a query runs:
import { useQuery } from '@tanstack/react-query'

function Post({ postId }) {
  // First query: fetch the post
  const { data: post } = useQuery({
    queryKey: ['post', postId],
    queryFn: () => fetchPost(postId),
  })

  // Second query: depends on post.userId
  const { data: user } = useQuery({
    queryKey: ['user', post?.userId],
    queryFn: () => fetchUser(post.userId),
    enabled: !!post?.userId, // Only run when post.userId exists
  })

  if (!post) return <div>Loading post...</div>

  return (
    <div>
      <h1>{post.title}</h1>
      {user && <p>By {user.name}</p>}
      <p>{post.body}</p>
    </div>
  )
}
The enabled option accepts a boolean or a function that returns a boolean. When false, the query will not execute and will stay in an idle state.

Enabled Option Types

The enabled option can be a boolean or a function:
// Boolean value
useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: !!userId,
})

// Function returning boolean
useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: (query) => {
    // Access the query instance for complex logic
    return !!userId && !query.state.error
  },
})

Multiple Dependencies

Chain multiple dependent queries:
function UserProfile({ userId }) {
  // First: fetch user
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId,
  })

  // Second: fetch user's company
  const { data: company } = useQuery({
    queryKey: ['company', user?.companyId],
    queryFn: () => fetchCompany(user.companyId),
    enabled: !!user?.companyId,
  })

  // Third: fetch company's projects
  const { data: projects } = useQuery({
    queryKey: ['projects', company?.id],
    queryFn: () => fetchCompanyProjects(company.id),
    enabled: !!company?.id,
  })

  return (
    <div>
      <h1>{user?.name}</h1>
      <h2>{company?.name}</h2>
      <ProjectList projects={projects} />
    </div>
  )
}
1

First query loads

The user query runs immediately when userId is available.
2

Second query waits

The company query waits until user.companyId is available.
3

Third query waits

The projects query waits until company.id is available.

Handling Loading States

Show appropriate loading states for dependent queries:
function Post({ postId }) {
  const { 
    data: post, 
    isPending: postPending,
    isError: postError 
  } = useQuery({
    queryKey: ['post', postId],
    queryFn: () => fetchPost(postId),
  })

  const { 
    data: author, 
    isPending: authorPending 
  } = useQuery({
    queryKey: ['user', post?.userId],
    queryFn: () => fetchUser(post.userId),
    enabled: !!post?.userId,
  })

  if (postPending) {
    return <div>Loading post...</div>
  }

  if (postError) {
    return <div>Error loading post</div>
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div className="author">
        {authorPending ? (
          <span>Loading author...</span>
        ) : (
          <span>By {author?.name}</span>
        )}
      </div>
      <p>{post.body}</p>
    </article>
  )
}
Show the main content as soon as it’s available, with a loading indicator for dependent data. This provides a better user experience than waiting for all data.

Conditional Queries

Enable queries based on user actions or application state:
function UserSettings({ userId }) {
  const [showPreferences, setShowPreferences] = useState(false)

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

  const { data: preferences } = useQuery({
    queryKey: ['preferences', userId],
    queryFn: () => fetchUserPreferences(userId),
    enabled: !!userId && showPreferences, // Only fetch when needed
  })

  return (
    <div>
      <h1>{user?.name}</h1>
      <button onClick={() => setShowPreferences(true)}>
        Show Preferences
      </button>
      {showPreferences && preferences && (
        <PreferencesList data={preferences} />
      )}
    </div>
  )
}
This pattern is useful for lazy-loading data that’s not immediately needed, reducing initial load time and server load.

Query Function with Enabled

Ensure the query function can safely execute when enabled:
const { data } = useQuery({
  queryKey: ['user', post?.userId],
  queryFn: ({ queryKey }) => {
    const [, userId] = queryKey
    if (!userId) {
      throw new Error('User ID is required')
    }
    return fetchUser(userId)
  },
  enabled: !!post?.userId,
})
Always ensure your query function can handle the data it needs. The enabled option prevents execution, but type safety may still require null checks.

Refetching Dependent Queries

Manually refetch dependent queries when needed:
function Post({ postId }) {
  const { data: post, refetch: refetchPost } = useQuery({
    queryKey: ['post', postId],
    queryFn: () => fetchPost(postId),
  })

  const { data: author, refetch: refetchAuthor } = useQuery({
    queryKey: ['user', post?.userId],
    queryFn: () => fetchUser(post.userId),
    enabled: !!post?.userId,
  })

  const refreshAll = async () => {
    await refetchPost()
    // Author will automatically refetch if userId changed
    // Or manually trigger:
    if (post?.userId) {
      await refetchAuthor()
    }
  }

  return (
    <div>
      <button onClick={refreshAll}>Refresh</button>
      {/* ... */}
    </div>
  )
}

Using with Dynamic Keys

Include all dependencies in the query key for proper caching:
function Comment({ commentId }) {
  const { data: comment } = useQuery({
    queryKey: ['comment', commentId],
    queryFn: () => fetchComment(commentId),
  })

  // Include both postId and userId in the key
  const { data: context } = useQuery({
    queryKey: ['comment-context', comment?.postId, comment?.userId],
    queryFn: () => fetchCommentContext({
      postId: comment.postId,
      userId: comment.userId,
    }),
    enabled: !!(comment?.postId && comment?.userId),
  })

  return (
    <div>
      <p>{comment?.text}</p>
      {context && <ContextInfo data={context} />}
    </div>
  )
}
Include all variables used in the query function in the query key. This ensures proper cache invalidation and prevents stale data issues.

Initial Data from Parent Query

Optimize by using data from a parent query as initial data:
function UserDetail({ userId }) {
  // Parent query with list of users
  const { data: users } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  // Detail query with initial data from list
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId,
    initialData: () => users?.find((u) => u.id === userId),
    // Mark as stale so it refetches in background
    staleTime: 0,
  })

  return <div>{user?.name}</div>
}
Using initialData from a parent query provides instant UI updates while still fetching fresh data in the background.

Dependent Mutations

Wait for a query before running a mutation:
function EditPost({ postId }) {
  const { data: post } = useQuery({
    queryKey: ['post', postId],
    queryFn: () => fetchPost(postId),
  })

  const updatePost = useMutation({
    mutationFn: (updates) => updatePostAPI(postId, updates),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['post', postId] })
    },
  })

  const handleSubmit = (formData) => {
    if (!post) {
      console.error('Post not loaded yet')
      return
    }
    updatePost.mutate(formData)
  }

  if (!post) return <div>Loading...</div>

  return <PostForm initialData={post} onSubmit={handleSubmit} />
}

Alternative: skipToken

Use skipToken to skip queries in a type-safe way:
import { useQuery, skipToken } from '@tanstack/react-query'

function Post({ postId }) {
  const { data: post } = useQuery({
    queryKey: ['post', postId],
    queryFn: postId ? () => fetchPost(postId) : skipToken,
  })

  return <div>{post?.title}</div>
}
skipToken is a type-safe alternative to the enabled option when you want to conditionally skip a query based on missing parameters.

Build docs developers (and LLMs) love