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, andplaceholderData - 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
Post a Comment