tanstack-query-setup

安装量: 41
排名: #17731

安装

npx skills add https://github.com/patricio0312rev/skills --skill 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 ( {children} ); }

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

export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); }

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 { 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 ; if (error) return ;

return (

    {users?.map((user) => (
  • {user.name}
  • ))}
); }

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

async function fetchUser(userId: string): Promise { 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 ;

return

{user?.name}
; }

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

async function fetchProducts(filters: ProductFilters): Promise { 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 { 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) => { e.preventDefault(); const formData = new FormData(e.currentTarget);

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

};

return (

{isError &&

{error.message}

}
); }

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

return useMutation({ mutationFn: async ({ id, data }: { id: string; data: Partial }) => { 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<Todo[]>(['todos']);

  // Optimistically update
  queryClient.setQueryData<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<Todo[]>(['todos']);

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

  queryClient.setQueryData<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<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 { 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(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 (

{data?.pages.map((page, pageIndex) => ( {page.posts.map((post) => ( ))} ))}

  <div ref={loadMoreRef} className="h-10">
    {isFetchingNextPage && <Spinner />}
  </div>
</div>

); }

Offset-Based Pagination export function useInfiniteProducts() { return useInfiniteQuery({ queryKey: ['products'], queryFn: async ({ pageParam = 0 }) => { const response = await fetch(/api/products?offset=${pageParam}&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 ( /users/${userId}} onMouseEnter={prefetch} onFocus={prefetch} > {children} ); }

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 ( ); }

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 ;

return (

{userQuery.data?.name}'s Posts

{postsQuery.isLoading ? ( ) : ( )}
); }

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 ;

return (

{users.map((user) => ( ))}
); }

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

返回排行榜