Files
2026-04-10 16:50:03 +07:00

5.4 KiB

Advanced Data Fetching Patterns with TanStack Query

Dependent Queries

Queries that depend on data from other queries:

// First query - Get user ID
const { data: user } = useQuery({
  queryKey: ['user'],
  queryFn: fetchCurrentUser
});

// Second query - Depends on user ID
const { data: posts } = useQuery({
  queryKey: ['posts', user?.id],
  queryFn: () => fetchUserPosts(user!.id),
  enabled: !!user // Only run when user is available
});

Parallel Queries

Fetch multiple independent queries simultaneously:

function Dashboard() {
  const queries = useQueries({
    queries: [
      { queryKey: ['stats'], queryFn: fetchStats },
      { queryKey: ['recentPosts'], queryFn: fetchRecentPosts },
      { queryKey: ['notifications'], queryFn: fetchNotifications }
    ]
  });

  const [statsQuery, postsQuery, notificationsQuery] = queries;

  if (queries.some(q => q.isLoading)) return <Loading />;

  return <Dashboard
    stats={statsQuery.data}
    posts={postsQuery.data}
    notifications={notificationsQuery.data}
  />;
}

Infinite Queries

For pagination and infinite scroll:

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage
} = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  initialPageParam: 1
});

// Flatten pages
const allPosts = data?.pages.flatMap(page => page.posts) ?? [];

return (
  <div>
    {allPosts.map(post => <PostCard key={post.id} post={post} />)}
    {hasNextPage && (
      <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
        {isFetchingNextPage ? 'Loading...' : 'Load More'}
      </button>
    )}
  </div>
);

Prefetching

Preload data before it's needed:

import { useQueryClient } from '@tanstack/react-query';

function PostLink({ postId }: { postId: string }) {
  const queryClient = useQueryClient();

  const handleMouseEnter = () => {
    queryClient.prefetchQuery({
      queryKey: ['post', postId],
      queryFn: () => fetchPost(postId)
    });
  };

  return (
    <Link to={`/posts/${postId}`} onMouseEnter={handleMouseEnter}>
      View Post
    </Link>
  );
}

Suspense Mode

Use with React Suspense:

import { useSuspenseQuery } from '@tanstack/react-query';

function PostDetails({ postId }: { postId: string }) {
  // Throws promise on loading, error on error
  const { data: post } = useSuspenseQuery({
    queryKey: ['post', postId],
    queryFn: () => fetchPost(postId)
  });

  return <div>{post.title}</div>;
}

// Wrap with Suspense
<Suspense fallback={<Loading />}>
  <PostDetails postId="123" />
</Suspense>

Query Cancellation

Cancel queries when component unmounts:

const { data, isLoading } = useQuery({
  queryKey: ['search', searchTerm],
  queryFn: async ({ signal }) => {
    const response = await fetch(`/api/search?q=${searchTerm}`, { signal });
    return response.json();
  }
});

Initial Data

Provide initial data to avoid loading state:

const { data } = useQuery({
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId),
  initialData: () => {
    // Get from cache or other source
    return queryClient.getQueryData(['posts'])?.find((post) => post.id === postId);
  }
});

Placeholder Data

Show placeholder while loading:

const { data, isPlaceholderData } = useQuery({
  queryKey: ['posts', page],
  queryFn: () => fetchPosts(page),
  placeholderData: (previousData) => previousData  // Keep previous page while loading
});

// Or provide static placeholder
placeholderData: { posts: [], total: 0 }

Optimistic Updates with Queries

Update UI immediately, rollback on error:

const queryClient = useQueryClient();

const { mutate } = useMutation({
  mutationFn: updatePost,
  onMutate: async (newPost) => {
    // Cancel outgoing queries
    await queryClient.cancelQueries({ queryKey: ['post', newPost.id] });

    // Snapshot current value
    const previousPost = queryClient.getQueryData(['post', newPost.id]);

    // Optimistically update
    queryClient.setQueryData(['post', newPost.id], newPost);

    return { previousPost };
  },
  onError: (err, newPost, context) => {
    // Rollback on error
    queryClient.setQueryData(['post', newPost.id], context?.previousPost);
  },
  onSettled: (newPost) => {
    // Refetch after success or error
    queryClient.invalidateQueries({ queryKey: ['post', newPost.id] });
  }
});

Query Retries

Configure retry behavior:

const { data } = useQuery({
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId),
  retry: 3, // Retry 3 times
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) // Exponential backoff
});

Error Handling

Handle query errors:

const { data, error, isError } = useQuery({
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId),
  throwOnError: false  // Don't throw, just set error
});

if (isError) {
  return <ErrorMessage error={error} />;
}

Best Practices

  1. Use Suspense - Better loading UX with React Suspense
  2. Prefetch on Intent - Preload data on hover/focus
  3. Enable Queries Conditionally - Use enabled option
  4. Cancel on Unmount - Use abort signals
  5. Handle Errors Gracefully - Show error states
  6. Optimize with Placeholders - Show previous data while loading