react-query-patterns

TanStack React Query Patterns

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 "react-query-patterns" with this command: npx skills add masanao-ohba/claude-manifests/masanao-ohba-claude-manifests-react-query-patterns

TanStack React Query Patterns

Setup

Query Client

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

Queries

Basic Query

Fetch data with automatic caching:

'use client';

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

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

export function UserProfile({ userId }: { userId: string }) { const { data, isLoading, error } = useQuery({ queryKey: ['user', userId], queryFn: async () => { const response = await fetch(/api/users/${userId}); if (!response.ok) throw new Error('Failed to fetch user'); return response.json() as Promise<User>; }, });

if (isLoading) return <LoadingSkeleton />; if (error) return <ErrorMessage error={error} />;

return <div>{data.name}</div>; }

Query with Options

const { data, isLoading, error, refetch } = useQuery({ queryKey: ['posts', { page, filter }], queryFn: () => fetchPosts(page, filter), enabled: !!userId, // Only run if userId exists staleTime: 5 * 60 * 1000, // Data fresh for 5 minutes gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes retry: 3, // Retry failed requests 3 times refetchInterval: 30 * 1000, // Refetch every 30 seconds refetchOnWindowFocus: true, // Refetch when window regains focus });

Query Keys

Structure

Hierarchical query key organization:

Pattern Example

Simple ['users']

With ID ['users', userId]

With params ['posts', { page, filter }]

Nested ['users', userId, 'posts']

Best Practices

  • Use arrays for all query keys

  • Order from general to specific

  • Include all variables that affect the query

  • Use objects for multiple parameters

  • Keep keys consistent across the app

Query Key Factory

// Query key organization const queryKeys = { users: ['users'] as const, user: (id: string) => ['users', id] as const, userPosts: (id: string) => ['users', id, 'posts'] as const, posts: { all: ['posts'] as const, lists: () => ['posts', 'list'] as const, list: (filters: PostFilters) => ['posts', 'list', filters] as const, detail: (id: string) => ['posts', id] as const, }, };

// Usage const { data } = useQuery({ queryKey: queryKeys.user(userId), queryFn: () => fetchUser(userId), });

Mutations

Basic Mutation

Modify server data:

import { useMutation, useQueryClient } from '@tanstack/react-query';

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

const mutation = useMutation({ mutationFn: async (newPost: NewPost) => { const response = await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newPost), }); if (!response.ok) throw new Error('Failed to create post'); return response.json(); }, onSuccess: () => { // Invalidate and refetch posts query queryClient.invalidateQueries({ queryKey: ['posts'] }); }, });

const handleSubmit = (data: NewPost) => { mutation.mutate(data); };

return ( <form onSubmit={handleSubmit}> {/* form fields */} {mutation.isPending && <p>Creating post...</p>} {mutation.isError && <p>Error: {mutation.error.message}</p>} {mutation.isSuccess && <p>Post created!</p>} </form> ); }

Optimistic Updates

Update UI immediately before server responds:

const mutation = useMutation({ mutationFn: updateTodo, onMutate: async (newTodo) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: ['todos'] });

// Snapshot current value
const previousTodos = queryClient.getQueryData(['todos']);

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

// Return context with snapshot
return { previousTodos };

}, onError: (err, newTodo, context) => { // Rollback on error queryClient.setQueryData(['todos'], context.previousTodos); }, onSettled: () => { // Refetch after error or success queryClient.invalidateQueries({ queryKey: ['todos'] }); }, });

Mutation with Invalidation

const mutation = useMutation({ mutationFn: deletePost, onSuccess: (_, deletedPostId) => { // Invalidate list query queryClient.invalidateQueries({ queryKey: ['posts', 'list'] });

// Remove deleted post from cache
queryClient.removeQueries({ queryKey: ['posts', deletedPostId] });

// Or update cache manually
queryClient.setQueryData(['posts', 'list'], (old: Post[]) => {
  return old.filter((post) => post.id !== deletedPostId);
});

}, });

Infinite Queries

Load more data as user scrolls:

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

export function InfinitePostList() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, } = useInfiniteQuery({ queryKey: ['posts', 'infinite'], queryFn: async ({ pageParam }) => { const response = await fetch(/api/posts?page=${pageParam}); return response.json(); }, initialPageParam: 1, getNextPageParam: (lastPage, allPages) => { return lastPage.hasMore ? allPages.length + 1 : undefined; }, });

if (isLoading) return <LoadingSkeleton />;

return ( <div> {data.pages.map((page, i) => ( <div key={i}> {page.posts.map((post) => ( <PostCard key={post.id} post={post} /> ))} </div> ))} {hasNextPage && ( <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}> {isFetchingNextPage ? 'Loading...' : 'Load More'} </button> )} </div> ); }

Prefetching

Hover Prefetch

Prefetch data on hover for instant navigation:

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

export function PostLink({ postId }: { postId: string }) { const queryClient = useQueryClient();

const prefetchPost = () => { queryClient.prefetchQuery({ queryKey: ['posts', postId], queryFn: () => fetchPost(postId), staleTime: 60 * 1000, // Keep for 1 minute }); };

return ( <Link href={/posts/${postId}} onMouseEnter={prefetchPost} onTouchStart={prefetchPost} > View Post </Link> ); }

Page Prefetch

const { data } = useQuery({ queryKey: ['posts', page], queryFn: () => fetchPosts(page), });

// Prefetch next page useEffect(() => { if (data?.hasMore) { queryClient.prefetchQuery({ queryKey: ['posts', page + 1], queryFn: () => fetchPosts(page + 1), }); } }, [data, page, queryClient]);

Dependent Queries

Query depends on result of previous query:

// First query const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), });

// Second query depends on first const { data: projects } = useQuery({ queryKey: ['projects', user?.id], queryFn: () => fetchUserProjects(user.id), enabled: !!user, // Only run when user exists });

Parallel Queries

Multiple Independent Queries

export function Dashboard() { const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers, });

const posts = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, });

const stats = useQuery({ queryKey: ['stats'], queryFn: fetchStats, });

if (users.isLoading || posts.isLoading || stats.isLoading) { return <LoadingSkeleton />; }

return ( <div> <UserList users={users.data} /> <PostList posts={posts.data} /> <Stats data={stats.data} /> </div> ); }

useQueries for Dynamic Queries

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

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

const isLoading = userQueries.some((query) => query.isLoading); const users = userQueries.map((query) => query.data);

if (isLoading) return <LoadingSkeleton />;

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

Error Handling

Retry Logic

const { data, error } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, retry: (failureCount, error) => { // Don't retry on 404 if (error.status === 404) return false; // Retry up to 3 times return failureCount < 3; }, retryDelay: (attemptIndex) => { // Exponential backoff: 1s, 2s, 4s return Math.min(1000 * 2 ** attemptIndex, 30000); }, });

Error Boundaries

import { useQuery } from '@tanstack/react-query'; import { useErrorBoundary } from 'react-error-boundary';

export function CriticalData() { const { showBoundary } = useErrorBoundary();

const { data } = useQuery({ queryKey: ['critical'], queryFn: fetchCriticalData, throwOnError: true, // Throw errors to Error Boundary });

return <div>{/* render data */}</div>; }

Best Practices

Do

  • Use query keys as array of dependencies

  • Invalidate queries after mutations

  • Prefetch on hover for better UX

  • Use staleTime to reduce unnecessary refetches

  • Implement optimistic updates for instant feedback

  • Use enabled option for dependent queries

  • Keep query functions pure and reusable

Don't

  • Don't use query keys as strings (use arrays)

  • Don't mutate query data directly

  • Don't forget to handle loading and error states

  • Don't set staleTime too low (causes excessive requests)

  • Don't invalidate all queries (be specific)

  • Don't put business logic in queryFn (use services)

Performance

Optimization

  • Set appropriate staleTime (avoid unnecessary refetches)

  • Use gcTime to control cache memory usage

  • Implement pagination or infinite queries for large lists

  • Prefetch predictable user navigation

  • Use select option to subscribe to only needed data

Monitoring

  • Enable React Query Devtools in development

  • Monitor network requests in DevTools

  • Check query cache size regularly

  • Measure query execution time

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.

General

requirement-analyzer

No summary provided by upstream source.

Repository SourceNeeds Review
General

test-case-designer

No summary provided by upstream source.

Repository SourceNeeds Review
General

test-validator

No summary provided by upstream source.

Repository SourceNeeds Review
General

functional-designer

No summary provided by upstream source.

Repository SourceNeeds Review