Skip to main content

🧱 Part 8 — Server State with TanStack Query (Typed Queries & Mutations) ⚙️✨


Client state (local UI) ≠ server state (data from APIs). TanStack Query (a.k.a. React Query) makes server state easy, fast, and type‑safe: caching, retries, background refresh, optimistic updates, and more.

🎯 Goals

  • Install & configure QueryClient with sensible defaults
  • Create typed query/mutation hooks that wrap our Axios API
  • Use cache keys, staleTime, select, and placeholderData
  • Implement optimistic updates with rollback
  • Add pagination and infinite scrolling patterns

We’ll reuse the Axios layer from Part 7 (/lib/api).


0) Install

pnpm add @tanstack/react-query

1) Provider setup (once at the app root)

// src/app/AppProviders.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,
      refetchOnWindowFocus: true,
      retry: (failureCount, error: any) => (error?.status === 404 ? false : failureCount < 3),
    },
    mutations: { retry: 0 },
  },
});

export function AppProviders({ children }: { children: ReactNode }) {
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}

2) Query keys (centralize for consistency)

// src/features/tasks/queryKeys.ts
export const qk = {
  all: ['tasks'] as const,
  list: (page: number) => [...qk.all, 'list', { page }] as const,
  byId: (id: string) => [...qk.all, 'byId', { id }] as const,
};

3) Typed queries (list + byId)

// src/features/tasks/queries.tsx
import { useQuery } from '@tanstack/react-query';
import { listTasks, getTask } from '@/lib/api/tasks';
import { qk } from './queryKeys';
import type { Task } from '@/lib/api/tasks.types';

export function useTasks(page: number) {
  return useQuery({
    queryKey: qk.list(page),
    queryFn: () => listTasks({ page, pageSize: 10 }),
    placeholderData: (prev) => prev,
    select: (data) => data.items,
  });
}

export function useTask(id: string) {
  return useQuery({
    queryKey: qk.byId(id),
    queryFn: () => getTask(id),
    enabled: Boolean(id),
  });
}

4) Typed mutations (create/update/toggle/delete) with optimistic updates

// src/features/tasks/mutations.tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createTask, updateTask, toggleTask, deleteTask } from '@/lib/api/tasks';
import { qk } from './queryKeys';
import type { Task } from '@/lib/api/tasks.types';

export function useCreateTask() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: createTask,
    onSuccess: () => {
      void qc.invalidateQueries({ queryKey: qk.all });
    },
  });
}

export function useToggleTask() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (id: string) => toggleTask(id),
    onMutate: async (id) => {
      await qc.cancelQueries({ queryKey: qk.all });
      const snapshots: Array<[readonly unknown[], unknown | undefined]> = [];

      qc.getQueriesData<Task[] | { items: Task[] }>(qk.all).forEach(([key, old]) => {
        snapshots.push([key, old]);
        if (!old) return;
        if (Array.isArray(old)) {
          qc.setQueryData(key, old.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
        } else if ('items' in old) {
          qc.setQueryData(key, {
            ...old,
            items: old.items.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
          });
        }
      });

      return { snapshots };
    },
    onError: (_err, _id, ctx) => {
      ctx?.snapshots.forEach(([key, old]) => qc.setQueryData(key, old));
    },
    onSettled: () => {
      void qc.invalidateQueries({ queryKey: qk.all });
    },
  });
}

export function useDeleteTask() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (id: string) => deleteTask(id),
    onSuccess: () => void qc.invalidateQueries({ queryKey: qk.all }),
  });
}

5) Wiring into a Tasks page

// src/pages/tasks/Tasks.tsx
import { useState } from 'react';
import { useTasks } from '@/features/tasks/queries';
import { useToggleTask, useDeleteTask } from '@/features/tasks/mutations';

export default function TasksPage() {
  const [page, setPage] = useState(1);
  const { data: tasks, isLoading, error } = useTasks(page);
  const toggle = useToggleTask();
  const del = useDeleteTask();

  if (isLoading) return <p>Loading…</p>;
  if (error) return <p className="text-red-600">❌ {(error as any).message}</p>;

  return (
    <section>
      <h2 className="text-xl font-semibold mb-2">Tasks</h2>
      <ul className="space-y-2">
        {tasks?.map((t) => (
          <li key={t.id} className="flex items-center gap-2">
            <input
              type="checkbox"
              checked={t.done}
              onChange={() => toggle.mutate(t.id)}
            />
            <span>{t.title}</span>
            <button onClick={() => del.mutate(t.id)} className="ml-auto text-red-600">
              Delete
            </button>
          </li>
        ))}
      </ul>
      <div className="mt-3 flex gap-2">
        <button onClick={() => setPage((p) => Math.max(1, p - 1))}>Prev</button>
        <button onClick={() => setPage((p) => p + 1)}>Next</button>
      </div>
    </section>
  );
}

6) Optional DevTools

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

// Inside AppProviders
<QueryClientProvider client={queryClient}>
  {children}
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>

✅ Wrap‑Up

You now have server state managed with TanStack Query:

  • Typed queries & mutations
  • Cache, retries, background refresh
  • Optimistic updates with rollback

👉 Next step: integrate Redux Toolkit for app‑level global state (non‑server data like theme, filters, UI flags).

Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...