5.3 KiB
5.3 KiB
Common TanStack Query Patterns
Reusable patterns for real-world applications
Pattern 1: Dependent Queries
Query B depends on data from Query A:
function UserPosts({ userId }) {
const { data: user } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
})
const { data: posts } = useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Wait for user
})
}
Pattern 2: Parallel Queries with useQueries
Fetch multiple resources in parallel:
function TodoDetails({ ids }) {
const results = useQueries({
queries: ids.map(id => ({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
})),
})
const isLoading = results.some(r => r.isPending)
const data = results.map(r => r.data)
}
Pattern 3: Paginated Queries with placeholderData
Keep previous data while fetching next page:
import { keepPreviousData } from '@tanstack/react-query'
function PaginatedTodos() {
const [page, setPage] = useState(0)
const { data } = useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData, // Keep old data while loading
})
}
Pattern 4: Infinite Scroll
Auto-load more data on scroll:
function InfiniteList() {
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['items'],
queryFn: ({ pageParam }) => fetchItems(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// Intersection Observer for auto-loading
const ref = useRef()
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => entry.isIntersecting && hasNextPage && fetchNextPage()
)
if (ref.current) observer.observe(ref.current)
return () => observer.disconnect()
}, [fetchNextPage, hasNextPage])
return (
<>
{data.pages.map(page => page.data.map(item => <div>{item}</div>))}
<div ref={ref}>Loading...</div>
</>
)
}
Pattern 5: Optimistic Updates
Instant UI feedback:
function useOptimisticToggle() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateTodo,
onMutate: async (updated) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previous = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], (old) =>
old.map(todo => todo.id === updated.id ? updated : todo)
)
return { previous }
},
onError: (err, vars, context) => {
queryClient.setQueryData(['todos'], context.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
Pattern 6: Prefetching on Hover
Load data before user clicks:
function TodoList() {
const queryClient = useQueryClient()
const prefetch = (id) => {
queryClient.prefetchQuery({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
})
}
return (
<ul>
{todos.map(todo => (
<li onMouseEnter={() => prefetch(todo.id)}>
<Link to={`/todos/${todo.id}`}>{todo.title}</Link>
</li>
))}
</ul>
)
}
Pattern 7: Search/Debounce
Debounced search with automatic cancellation:
import { useState, useDeferredValue } from 'react'
function Search() {
const [search, setSearch] = useState('')
const deferredSearch = useDeferredValue(search)
const { data } = useQuery({
queryKey: ['search', deferredSearch],
queryFn: ({ signal }) =>
fetch(`/api/search?q=${deferredSearch}`, { signal }).then(r => r.json()),
enabled: deferredSearch.length >= 2,
})
}
Pattern 8: Polling/Refetch Interval
Auto-refetch every N seconds:
const { data } = useQuery({
queryKey: ['stock-price'],
queryFn: fetchStockPrice,
refetchInterval: 1000 * 30, // Every 30 seconds
refetchIntervalInBackground: true, // Even when tab inactive
})
Pattern 9: Conditional Fetching
Only fetch when needed:
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId && isAuthenticated,
})
Pattern 10: Initial Data from Cache
Use cached data as initial value:
const { data: todo } = useQuery({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
initialData: () => {
return queryClient
.getQueryData(['todos'])
?.find(t => t.id === id)
},
})
Pattern 11: Mutation with Multiple Invalidations
Update multiple related queries:
useMutation({
mutationFn: updateTodo,
onSuccess: (updated) => {
queryClient.setQueryData(['todos', updated.id], updated)
queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.invalidateQueries({ queryKey: ['stats'] })
queryClient.invalidateQueries({ queryKey: ['users', updated.userId] })
},
})
Pattern 12: Global Error Handler
Centralized error handling:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
toast.error(error.message)
logToSentry(error)
},
},
mutations: {
onError: (error) => {
toast.error('Action failed')
logToSentry(error)
},
},
},
})