DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Tanstack Query: Server State Management That Replaces Redux

Tanstack Query: Server State Management That Replaces Redux

Redux is overkill for 90% of apps. Most state isn't client state — it's server state: data fetched from an API. Tanstack Query manages server state with caching, background refetching, and optimistic updates.

Install

npm install @tanstack/react-query
npm install --save-dev @tanstack/react-query-devtools
Enter fullscreen mode Exit fullscreen mode

Setup

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes before refetch
      retry: 2,
    },
  },
});

export function Providers({ children }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools />
    </QueryClientProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Fetching Data

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

function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  });

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorMessage />;

  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Mutations

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

function UpdateNameForm({ userId }) {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (name: string) =>
      fetch(`/api/users/${userId}`, {
        method: 'PATCH',
        body: JSON.stringify({ name }),
      }).then(r => r.json()),
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['user', userId] });
    },
  });

  return (
    <button
      onClick={() => mutation.mutate('New Name')}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Saving...' : 'Save'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Optimistic Updates

const toggleTodo = useMutation({
  mutationFn: (id: string) => fetch(`/api/todos/${id}/toggle`, { method: 'POST' }),
  onMutate: async (id) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] });
    const previous = queryClient.getQueryData(['todos']);

    // Optimistically update
    queryClient.setQueryData(['todos'], (old: Todo[]) =>
      old.map(t => t.id === id ? { ...t, done: !t.done } : t)
    );
    return { previous };
  },
  onError: (err, id, context) => {
    // Roll back on error
    queryClient.setQueryData(['todos'], context?.previous);
  },
});
Enter fullscreen mode Exit fullscreen mode

Infinite Scroll

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam = 1 }) =>
    fetch(`/api/posts?page=${pageParam}`).then(r => r.json()),
  getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
});

const allPosts = data?.pages.flatMap(p => p.posts) ?? [];
Enter fullscreen mode Exit fullscreen mode

Prefetching

// Prefetch on hover for instant navigation
function PostLink({ postId }) {
  const queryClient = useQueryClient();

  return (
    <Link
      href={`/posts/${postId}`}
      onMouseEnter={() =>
        queryClient.prefetchQuery({
          queryKey: ['post', postId],
          queryFn: () => getPost(postId),
        })
      }
    >
      View Post
    </Link>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tanstack Query ships pre-configured in the AI SaaS Starter Kit — QueryClient setup, custom hooks for all API calls, devtools enabled in dev. $99 at whoffagents.com.

Top comments (0)