Loading...
Loading...
Expert in TanStack Query (React Query) — asynchronous state management. Covers data fetching, stale time configuration, mutations, optimistic updates, and Next.js App Router (SSR) integration.
npx skill4agent add sickn33/antigravity-awesome-skills tanstack-query-expertuseEffectuseStatestaleTimegcTimeretryuseMutationqueryClient.invalidateQueriesuseEffectuseQueryimport { useQuery } from '@tanstack/react-query';
// 1. Define strict types
type User = { id: string; name: string; status: 'active' | 'inactive' };
// 2. Define the fetcher function
const fetchUser = async (userId: string): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
};
// 3. Export a custom hook
export const useUser = (userId: string) => {
return useQuery({
queryKey: ['users', userId], // Array-based query key
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes (no background refetching)
enabled: !!userId, // Dependent query: only run if userId exists
});
};// Filtering / Sorting
useQuery({
queryKey: ['issues', { status: 'open', sort: 'desc' }],
queryFn: () => fetchIssues({ status: 'open', sort: 'desc' })
});
// Factory pattern for query keys (Highly recommended for large apps)
export const issueKeys = {
all: ['issues'] as const,
lists: () => [...issueKeys.all, 'list'] as const,
list: (filters: string) => [...issueKeys.lists(), { filters }] as const,
details: () => [...issueKeys.all, 'detail'] as const,
detail: (id: number) => [...issueKeys.details(), id] as const,
};import { useMutation, useQueryClient } from '@tanstack/react-query';
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newPost: { title: string }) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
return res.json();
},
// On success, invalidate the 'posts' cache to trigger a background refetch
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
};export const useUpdateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodoFn,
// 1. Triggered immediately when mutate() is called
onMutate: async (newTodo) => {
// Cancel any outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot the previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update to the new value
queryClient.setQueryData(['todos'], (old: any) =>
old.map((todo: any) => todo.id === newTodo.id ? { ...todo, ...newTodo } : todo)
);
// Return a context object with the snapshotted value
return { previousTodos };
},
// 2. If the mutation fails, use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
// 3. Always refetch after error or success to ensure server sync
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
};// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false, // Prevents aggressive refetching on tab switch
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}initialData// app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import PostsList from './PostsList'; // Client Component
export default async function PostsPage() {
const queryClient = new QueryClient();
// Prefetch the data on the server
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPostsServerSide,
});
// Dehydrate the cache and pass it to the HydrationBoundary
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostsList />
</HydrationBoundary>
);
}// app/posts/PostsList.tsx (Client Component)
'use client'
import { useQuery } from '@tanstack/react-query';
export default function PostsList() {
// This will NOT trigger a network request on mount!
// It reads instantly from the dehydrated server cache.
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsClientSide,
});
return <div>{data.map(post => <p key={post.id}>{post.title}</p>)}</div>;
}['users']['user']staleTime1000 * 60staleTime0queryClient.setQueryDatainvalidateQueriesuseMutationuseQueryconst { mutate } = useCreatePost()useQueryqueryKeyuseEffect(() => setLocalState(data), [data])queryFnfetchuseEffectretry: falsestaleTimegcTimecacheTimestaleTimegcTimegcTimestaleTime