tanstack-query-setup

Manage server state with powerful caching, background updates, and optimistic UI.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack-query-setup" with this command: npx skills add monkey1sai/openai-cli/monkey1sai-openai-cli-tanstack-query-setup

TanStack Query Setup

Manage server state with powerful caching, background updates, and optimistic UI.

Core Workflow

  • Install and configure: Set up QueryClient

  • Create queries: Define data fetching hooks

  • Add mutations: Handle data modifications

  • Enable caching: Configure stale times

  • Implement optimistic updates: Instant UI feedback

  • Add infinite queries: Pagination and infinite scroll

Installation

npm install @tanstack/react-query @tanstack/react-query-devtools

Provider Setup

Next.js App Router

// app/providers.tsx 'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { useState } from 'react';

export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState( () => new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, // 1 minute gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime) retry: 1, refetchOnWindowFocus: false, }, }, }) );

return ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); }

// app/layout.tsx import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> ); }

Basic Queries

Simple Query

// hooks/useUsers.ts import { useQuery } from '@tanstack/react-query';

interface User { id: string; name: string; email: string; }

async function fetchUsers(): Promise<User[]> { const response = await fetch('/api/users'); if (!response.ok) { throw new Error('Failed to fetch users'); } return response.json(); }

export function useUsers() { return useQuery({ queryKey: ['users'], queryFn: fetchUsers, }); }

// Usage function UsersList() { const { data: users, isLoading, error } = useUsers();

if (isLoading) return <Spinner />; if (error) return <Error message={error.message} />;

return ( <ul> {users?.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }

Query with Parameters

// hooks/useUser.ts import { useQuery } from '@tanstack/react-query';

async function fetchUser(userId: string): Promise<User> { const response = await fetch(/api/users/${userId}); if (!response.ok) { throw new Error('Failed to fetch user'); } return response.json(); }

export function useUser(userId: string) { return useQuery({ queryKey: ['users', userId], queryFn: () => fetchUser(userId), enabled: !!userId, // Only fetch when userId exists }); }

// Usage function UserProfile({ userId }: { userId: string }) { const { data: user, isLoading } = useUser(userId);

if (isLoading) return <Skeleton />;

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

Query with Filters

// hooks/useProducts.ts interface ProductFilters { category?: string; minPrice?: number; maxPrice?: number; search?: string; }

async function fetchProducts(filters: ProductFilters): Promise<Product[]> { const params = new URLSearchParams(); if (filters.category) params.set('category', filters.category); if (filters.minPrice) params.set('minPrice', String(filters.minPrice)); if (filters.maxPrice) params.set('maxPrice', String(filters.maxPrice)); if (filters.search) params.set('search', filters.search);

const response = await fetch(/api/products?${params}); return response.json(); }

export function useProducts(filters: ProductFilters) { return useQuery({ queryKey: ['products', filters], queryFn: () => fetchProducts(filters), placeholderData: (previousData) => previousData, // Keep previous data while fetching }); }

Mutations

Basic Mutation

// hooks/useCreateUser.ts import { useMutation, useQueryClient } from '@tanstack/react-query';

interface CreateUserDto { name: string; email: string; }

async function createUser(data: CreateUserDto): Promise<User> { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), });

if (!response.ok) { throw new Error('Failed to create user'); }

return response.json(); }

export function useCreateUser() { const queryClient = useQueryClient();

return useMutation({ mutationFn: createUser, onSuccess: (newUser) => { // Invalidate and refetch users list queryClient.invalidateQueries({ queryKey: ['users'] }); }, onError: (error) => { console.error('Failed to create user:', error); }, }); }

// Usage function CreateUserForm() { const { mutate, isPending, isError, error } = useCreateUser();

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.currentTarget);

mutate({
  name: formData.get('name') as string,
  email: formData.get('email') as string,
});

};

return ( <form onSubmit={handleSubmit}> <input name="name" required /> <input name="email" type="email" required /> <button type="submit" disabled={isPending}> {isPending ? 'Creating...' : 'Create User'} </button> {isError && <p className="text-red-500">{error.message}</p>} </form> ); }

Update and Delete

// hooks/useUpdateUser.ts export function useUpdateUser() { const queryClient = useQueryClient();

return useMutation({ mutationFn: async ({ id, data }: { id: string; data: Partial<User> }) => { const response = await fetch(/api/users/${id}, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); return response.json(); }, onSuccess: (updatedUser) => { // Update the single user cache queryClient.setQueryData(['users', updatedUser.id], updatedUser); // Invalidate the list queryClient.invalidateQueries({ queryKey: ['users'] }); }, }); }

// hooks/useDeleteUser.ts export function useDeleteUser() { const queryClient = useQueryClient();

return useMutation({ mutationFn: async (userId: string) => { await fetch(/api/users/${userId}, { method: 'DELETE' }); return userId; }, onSuccess: (deletedId) => { // Remove from cache queryClient.removeQueries({ queryKey: ['users', deletedId] }); // Invalidate list queryClient.invalidateQueries({ queryKey: ['users'] }); }, }); }

Optimistic Updates

List Update

// hooks/useToggleTodo.ts export function useToggleTodo() { const queryClient = useQueryClient();

return useMutation({ mutationFn: async ({ id, completed }: { id: string; completed: boolean }) => { const response = await fetch(/api/todos/${id}, { method: 'PATCH', body: JSON.stringify({ completed }), }); return response.json(); },

// Optimistic update
onMutate: async ({ id, completed }) => {
  // Cancel outgoing refetches
  await queryClient.cancelQueries({ queryKey: ['todos'] });

  // Snapshot previous value
  const previousTodos = queryClient.getQueryData&#x3C;Todo[]>(['todos']);

  // Optimistically update
  queryClient.setQueryData&#x3C;Todo[]>(['todos'], (old) =>
    old?.map((todo) =>
      todo.id === id ? { ...todo, completed } : todo
    )
  );

  // Return context for rollback
  return { previousTodos };
},

// Rollback on error
onError: (err, variables, context) => {
  if (context?.previousTodos) {
    queryClient.setQueryData(['todos'], context.previousTodos);
  }
},

// Refetch after success or error
onSettled: () => {
  queryClient.invalidateQueries({ queryKey: ['todos'] });
},

}); }

Create with Optimistic Add

export function useCreateTodo() { const queryClient = useQueryClient();

return useMutation({ mutationFn: async (text: string) => { const response = await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }), }); return response.json(); },

onMutate: async (text) => {
  await queryClient.cancelQueries({ queryKey: ['todos'] });

  const previousTodos = queryClient.getQueryData&#x3C;Todo[]>(['todos']);

  // Add optimistic todo with temp id
  const optimisticTodo: Todo = {
    id: `temp-${Date.now()}`,
    text,
    completed: false,
  };

  queryClient.setQueryData&#x3C;Todo[]>(['todos'], (old) => [
    ...(old || []),
    optimisticTodo,
  ]);

  return { previousTodos, optimisticTodo };
},

onError: (err, text, context) => {
  if (context?.previousTodos) {
    queryClient.setQueryData(['todos'], context.previousTodos);
  }
},

onSuccess: (newTodo, text, context) => {
  // Replace optimistic todo with real one
  queryClient.setQueryData&#x3C;Todo[]>(['todos'], (old) =>
    old?.map((todo) =>
      todo.id === context?.optimisticTodo.id ? newTodo : todo
    )
  );
},

}); }

Infinite Queries

Cursor-Based Pagination

// hooks/useInfinitePosts.ts import { useInfiniteQuery } from '@tanstack/react-query';

interface PostsPage { posts: Post[]; nextCursor?: string; }

async function fetchPosts({ pageParam }: { pageParam?: string }): Promise<PostsPage> { const url = pageParam ? /api/posts?cursor=${pageParam} : '/api/posts';

const response = await fetch(url); return response.json(); }

export function useInfinitePosts() { return useInfiniteQuery({ queryKey: ['posts'], queryFn: fetchPosts, initialPageParam: undefined, getNextPageParam: (lastPage) => lastPage.nextCursor, }); }

// Usage with intersection observer function PostsFeed() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfinitePosts();

const loadMoreRef = useRef<HTMLDivElement>(null);

useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, { threshold: 0.1 } );

if (loadMoreRef.current) {
  observer.observe(loadMoreRef.current);
}

return () => observer.disconnect();

}, [fetchNextPage, hasNextPage, isFetchingNextPage]);

return ( <div> {data?.pages.map((page, pageIndex) => ( <Fragment key={pageIndex}> {page.posts.map((post) => ( <PostCard key={post.id} post={post} /> ))} </Fragment> ))}

  &#x3C;div ref={loadMoreRef} className="h-10">
    {isFetchingNextPage &#x26;&#x26; &#x3C;Spinner />}
  &#x3C;/div>
&#x3C;/div>

); }

Offset-Based Pagination

export function useInfiniteProducts() { return useInfiniteQuery({ queryKey: ['products'], queryFn: async ({ pageParam = 0 }) => { const response = await fetch(/api/products?offset=${pageParam}&#x26;limit=20); return response.json(); }, initialPageParam: 0, getNextPageParam: (lastPage, allPages) => { // Stop if less than limit returned if (lastPage.products.length < 20) return undefined; return allPages.length * 20; }, }); }

Query Factories

Organized Query Keys

// lib/queries/users.ts import { queryOptions } from '@tanstack/react-query';

export const userQueries = { all: () => queryOptions({ queryKey: ['users'], queryFn: fetchUsers, }),

detail: (id: string) => queryOptions({ queryKey: ['users', id], queryFn: () => fetchUser(id), }),

list: (filters: UserFilters) => queryOptions({ queryKey: ['users', 'list', filters], queryFn: () => fetchUsers(filters), }),

posts: (userId: string) => queryOptions({ queryKey: ['users', userId, 'posts'], queryFn: () => fetchUserPosts(userId), }), };

// Usage function UserProfile({ userId }: { userId: string }) { const userQuery = useQuery(userQueries.detail(userId)); const postsQuery = useQuery(userQueries.posts(userId)); // ... }

// Invalidation queryClient.invalidateQueries({ queryKey: ['users'] }); // All user queries queryClient.invalidateQueries({ queryKey: ['users', userId] }); // Specific user

Prefetching

On Hover

function UserLink({ userId, children }: { userId: string; children: React.ReactNode }) { const queryClient = useQueryClient();

const prefetch = () => { queryClient.prefetchQuery(userQueries.detail(userId)); };

return ( <Link href={/users/${userId}} onMouseEnter={prefetch} onFocus={prefetch} > {children} </Link> ); }

In Server Components

// app/users/page.tsx import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; import { userQueries } from '@/lib/queries/users'; import { UsersList } from './UsersList';

export default async function UsersPage() { const queryClient = new QueryClient();

await queryClient.prefetchQuery(userQueries.all());

return ( <HydrationBoundary state={dehydrate(queryClient)}> <UsersList /> </HydrationBoundary> ); }

Dependent Queries

function UserPosts({ userId }: { userId: string }) { // First query const userQuery = useQuery({ queryKey: ['users', userId], queryFn: () => fetchUser(userId), });

// Dependent query - only runs when user is loaded const postsQuery = useQuery({ queryKey: ['users', userId, 'posts'], queryFn: () => fetchUserPosts(userId), enabled: !!userQuery.data, // Wait for user });

if (userQuery.isLoading) return <Spinner />;

return ( <div> <h2>{userQuery.data?.name}'s Posts</h2> {postsQuery.isLoading ? ( <Spinner /> ) : ( <PostsList posts={postsQuery.data} /> )} </div> ); }

Parallel Queries

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

function Dashboard({ userIds }: { userIds: string[] }) { const userQueries = useQueries({ queries: userIds.map((id) => ({ queryKey: ['users', id], queryFn: () => fetchUser(id), })), });

const isLoading = userQueries.some((q) => q.isLoading); const users = userQueries.map((q) => q.data).filter(Boolean);

if (isLoading) return <Spinner />;

return ( <div> {users.map((user) => ( <UserCard key={user.id} user={user} /> ))} </div> ); }

Best Practices

  • Use query factories: Organized, reusable query options

  • Set appropriate stale times: Balance freshness vs performance

  • Optimistic updates: Instant UI feedback

  • Prefetch on hover: Anticipate user navigation

  • Use placeholderData: Show stale data while fetching

  • Handle errors gracefully: Error boundaries and retry

  • SSR with HydrationBoundary: Hydrate queries from server

  • Separate queries and mutations: Clear data flow

Output Checklist

Every TanStack Query implementation should include:

  • QueryClient with default options

  • Provider with devtools

  • Query factories for organization

  • Proper query keys structure

  • Mutations with invalidation

  • Optimistic updates for UX

  • Loading and error states

  • Prefetching strategy

  • SSR hydration (if using Next.js)

  • Infinite queries for pagination

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

websocket-realtime-builder

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

api-docs-generator

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

modal-drawer-system

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

multi-tenant-safety-checker

No summary provided by upstream source.

Repository SourceNeeds Review