TanStack Query Introduction - Data Fetching

intermediate | 45 min read | 2025.12.06

What You’ll Learn in This Tutorial

✓ TanStack Query basics
✓ Fetching data with useQuery
✓ Updates with useMutation
✓ Caching and revalidation
✓ Optimistic updates

Step 1: Setup

npm install @tanstack/react-query
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient();

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools />
    </QueryClientProvider>
  );
}

Step 2: Basic Query

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

function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const res = await fetch('/api/users');
      return res.json();
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.map((user: any) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Step 3: Query with Parameters

function UserDetail({ userId }: { userId: string }) {
  const { data, isLoading } = useQuery({
    queryKey: ['users', userId],
    queryFn: async () => {
      const res = await fetch(`/api/users/${userId}`);
      return res.json();
    },
    enabled: !!userId, // Only execute when userId exists
  });

  if (isLoading) return <div>Loading...</div>;

  return <div>{data?.name}</div>;
}

Step 4: Mutation

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

function CreateUserForm() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newUser: { name: string; email: string }) => {
      const res = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
      });
      return res.json();
    },
    onSuccess: () => {
      // Invalidate cache and refetch
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    mutation.mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" />
      <input name="email" placeholder="Email" />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Creating...' : 'Create User'}
      </button>
    </form>
  );
}

Step 5: Optimistic Updates

const mutation = useMutation({
  mutationFn: updateUser,
  onMutate: async (newData) => {
    await queryClient.cancelQueries({ queryKey: ['users', newData.id] });

    const previousUser = queryClient.getQueryData(['users', newData.id]);

    queryClient.setQueryData(['users', newData.id], newData);

    return { previousUser };
  },
  onError: (err, newData, context) => {
    queryClient.setQueryData(['users', newData.id], context?.previousUser);
  },
  onSettled: (data, error, variables) => {
    queryClient.invalidateQueries({ queryKey: ['users', variables.id] });
  },
});

Step 6: Infinite Scroll

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

function InfiniteList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useInfiniteQuery({
      queryKey: ['posts'],
      queryFn: async ({ pageParam = 0 }) => {
        const res = await fetch(`/api/posts?cursor=${pageParam}`);
        return res.json();
      },
      getNextPageParam: (lastPage) => lastPage.nextCursor,
    });

  return (
    <div>
      {data?.pages.map((page) =>
        page.items.map((item: any) => <div key={item.id}>{item.title}</div>)
      )}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : 'Load More'}
      </button>
    </div>
  );
}

Summary

TanStack Query simplifies server state management and makes caching, revalidation, and optimistic updates easy to implement.

← Back to list