Home
Contact
Blog
Playground
BlogCrash CourseTanStack Query — The Complete Crash Course

Published September 22, 2026

TanStack Query — The Complete Crash Course

Everything you need to actually use TanStack Query in production: queries, mutations, caching, invalidation, optimistic updates, and the pitfalls that trip everyone up.

ZB
Ziane BadreddineAuthor

This is a crash course, not a reference doc — every section is runnable code you can paste and adapt. By the end you'll understand queries, mutations, cache invalidation, and the three bugs everyone hits in their first week.

Why TanStack Query exists

Before it, "data fetching" in React meant useEffect + useState + manually tracking loading/error/stale state, times every component that needed the same data. TanStack Query replaces all of that with a declarative cache: you describe what you need, it handles when to fetch, refetch, and dedupe.

Setup

Install it:

npm install @tanstack/react-query

Wrap your app in a provider:

app/providers.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";

export function QueryProvider({ children }: { children: React.ReactNode }) {
  const [client] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={client}>{children}</QueryClientProvider>
  );
}

Your first query

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

function Profile({ userId }: { userId: string }) {
  const { data, isPending, error } = useQuery({
    queryKey: ["user", userId],
    queryFn: async () => {
      const res = await fetch(`/api/users/${userId}`);
      if (!res.ok) throw new Error("Failed to fetch user");
      return res.json();
    },
  });

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

  return <h1>{data.name}</h1>;
}

The queryKey array is the cache key. ["user", userId] means every distinct userId gets its own cache entry — call this hook twice with the same userId anywhere in your tree, and only one network request fires.

Mutations — writing data

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

function EditProfileForm({ userId }: { userId: string }) {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (data: { name: string }) => {
      const res = await fetch(`/api/users/${userId}`, {
        method: "PATCH",
        body: JSON.stringify(data),
      });
      return res.json();
    },
    onSuccess: () => {
      // invalidate so the profile query refetches fresh data
      queryClient.invalidateQueries({ queryKey: ["user", userId] });
    },
  });

  return (
    <button onClick={() => mutation.mutate({ name: "New Name" })}>
      {mutation.isPending ? "Saving..." : "Save"}
    </button>
  );
}

Optimistic updates

The pattern that makes UIs feel instant — update the cache before the server confirms, then roll back on failure:

const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ["todos"] });
    const previous = queryClient.getQueryData(["todos"]);

    queryClient.setQueryData(["todos"], (old: Todo[]) =>
      old.map((t) => (t.id === newTodo.id ? newTodo : t)),
    );

    return { previous };
  },
  onError: (_err, _newTodo, context) => {
    // roll back on failure
    queryClient.setQueryData(["todos"], context?.previous);
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ["todos"] });
  },
});

Always cancel in-flight queries with cancelQueries before writing optimistic data — otherwise a slow, stale fetch can resolve after your optimistic update and silently overwrite it.

The three bugs everyone hits

Should you still use useEffect for fetching?

No. If you're fetching data in useEffect + useState in 2026, TanStack Query (or your framework's built-in loader, if using Next.js Server Components) should replace it — you get caching, deduping, retries, and refetch-on-focus for free.

Summary

  • useQuery reads, useMutation writes.
  • queryKey is your cache identity — include every input.
  • invalidateQueries is how you tell the cache "this is stale."
  • Optimistic updates need onMutate + onError rollback + onSettled refetch, in that order.

On this page

Why TanStack Query existsSetupYour first queryMutations — writing dataOptimistic updatesThe three bugs everyone hitsShould you still use useEffect for fetching?Summary

Related Blogs

Sep 5,2026

Comparisons

React vs Vue in 2026 — An Honest Comparison

Component model, reactivity, ecosystem, and hiring — a practical comparison of React and Vue for teams choosing in 2026, without the tribalism.

React

Vue

Frontend

Comparison

Navigation

  • Home
  • About
  • Projects
  • Contact

Explore

  • Experience
  • Education
  • Activities

Blog

  • Why I Chose Opaque Tokens Over JWT for My Spring Boot API
  • Java Interview Questions & Answers — My Prep Notes
  • TanStack Query — The Complete Crash Course
  • Motion (ex Framer Motion): Animations, UX, and Performance
  • Why I Stopped Optimizing Early
  • Is Bun Ready for Production in 2026?
  • React vs Vue in 2026 — An Honest Comparison
  • All posts

Language

Connect

GitHub
LinkedIn
Email

Ziane Badr Eddine.