Skip to main content
Glama
tanstack-query.txt•14 kB
# TanStack Query **Powerful asynchronous state management for TS/JS** TanStack Query (formerly React Query) is a powerful data fetching and state management library that simplifies server state management in modern web applications. It provides automatic caching, background updates, optimistic updates, and much more out of the box. ## Installation ```bash npm install @tanstack/react-query ``` ## Setup ### QueryClient Provider Wrap your application with QueryClientProvider: ```typescript import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5 minutes refetchOnWindowFocus: false, }, }, }) function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> </QueryClientProvider> ) } ``` ## Core Concepts ### Queries Queries are used for fetching data. They automatically cache results and manage loading/error states. ### Basic Query ```typescript import { useQuery } from '@tanstack/react-query' function Todos() { const { data, isLoading, isError, error } = useQuery({ queryKey: ['todos'], queryFn: async () => { const response = await fetch('/api/todos') if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, }) if (isLoading) return <div>Loading...</div> if (isError) return <div>Error: {error.message}</div> return ( <ul> {data.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ) } ``` ### Query Keys Query keys uniquely identify queries and enable automatic refetching and caching: ```typescript // Simple key useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // Key with parameters useQuery({ queryKey: ['todo', todoId], queryFn: () => fetchTodo(todoId) }) // Key with multiple parameters useQuery({ queryKey: ['todos', { status, page }], queryFn: () => fetchTodos({ status, page }) }) ``` ### Query Options ```typescript useQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: 5 * 60 * 1000, // Data considered fresh for 5 minutes gcTime: 10 * 60 * 1000, // Cache kept for 10 minutes (formerly cacheTime) refetchOnMount: true, // Refetch on component mount refetchOnWindowFocus: false, // Don't refetch when window regains focus refetchInterval: 30000, // Refetch every 30 seconds retry: 3, // Retry failed requests 3 times enabled: !!userId, // Only run query if userId exists }) ``` ## Mutations Mutations are used for creating, updating, or deleting data. ### Basic Mutation ```typescript import { useMutation, useQueryClient } from '@tanstack/react-query' function AddTodo() { const queryClient = useQueryClient() const mutation = useMutation({ mutationFn: async (newTodo: string) => { const response = await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text: newTodo }), headers: { 'Content-Type': 'application/json' }, }) return response.json() }, onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) return ( <div> {mutation.isLoading && <p>Adding todo...</p>} {mutation.isError && <p>Error: {mutation.error.message}</p>} {mutation.isSuccess && <p>Todo added!</p>} <button onClick={() => mutation.mutate('New todo item')} disabled={mutation.isLoading} > Add Todo </button> </div> ) } ``` ### Mutation with Error Handling ```typescript const mutation = useMutation({ mutationFn: updateTodo, onSuccess: (data, variables, context) => { // data: response from mutationFn // variables: arguments passed to mutate() // context: value returned from onMutate queryClient.invalidateQueries({ queryKey: ['todos'] }) }, onError: (error, variables, context) => { // Handle error console.error('Mutation failed:', error) }, onSettled: (data, error, variables, context) => { // Always runs after success or error console.log('Mutation settled') }, }) ``` ## Optimistic Updates Update UI immediately before server confirms the change: ```typescript const addTodoMutation = useMutation({ mutationFn: async (newTodo: string) => { const response = await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text: newTodo }), }) return response.json() }, onMutate: async (newTodo: string) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: ['todos'] }) // Snapshot previous value const previousTodos = queryClient.getQueryData(['todos']) // Optimistically update cache queryClient.setQueryData(['todos'], (old: any) => ({ ...old, items: [...old.items, { id: Math.random(), text: newTodo }], })) // Return context with snapshot return { previousTodos } }, onError: (err, newTodo, context) => { // Rollback on error if (context?.previousTodos) { queryClient.setQueryData(['todos'], context.previousTodos) } }, onSettled: () => { // Always refetch after error or success queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) ``` ### Simplified Optimistic Updates Use mutation variables for UI updates without direct cache manipulation: ```typescript const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) // In render {queryInfo.data.items.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} {addTodoMutation.isPending && ( <li key={String(addTodoMutation.submittedAt)} style={{ opacity: 0.5 }}> {addTodoMutation.variables} </li> )} ``` ## Query Invalidation Invalidation marks queries as stale and refetches them: ```typescript const queryClient = useQueryClient() // Invalidate single query await queryClient.invalidateQueries({ queryKey: ['todos'] }) // Invalidate multiple queries await Promise.all([ queryClient.invalidateQueries({ queryKey: ['todos'] }), queryClient.invalidateQueries({ queryKey: ['reminders'] }), ]) // Invalidate all queries with a prefix queryClient.invalidateQueries({ queryKey: ['todos'] }) // Matches ['todos', 1], ['todos', 2], etc. ``` ## Manual Cache Updates Directly update cache without refetching: ```typescript const mutation = useMutation({ mutationFn: editTodo, onSuccess: (data, variables) => { // Update specific query queryClient.setQueryData(['todo', { id: variables.id }], data) }, }) // Or manually queryClient.setQueryData(['todos'], (oldData) => { return { ...oldData, items: [...oldData.items, newTodo], } }) ``` ## Prefetching Load data before it's needed: ```typescript const queryClient = useQueryClient() // Prefetch on hover <button onMouseEnter={() => { queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id), }) }} > View Todo </button> // Prefetch in advance useEffect(() => { // Prefetch next page if (data?.hasNextPage) { queryClient.prefetchQuery({ queryKey: ['todos', page + 1], queryFn: () => fetchTodos(page + 1), }) } }, [data, page]) ``` ## Infinite Queries For pagination and infinite scrolling: ```typescript import { useInfiniteQuery } from '@tanstack/react-query' function InfiniteTodos() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['todos'], queryFn: ({ pageParam = 0 }) => fetchTodos(pageParam), getNextPageParam: (lastPage, pages) => lastPage.nextCursor, initialPageParam: 0, }) return ( <> {data?.pages.map((page, i) => ( <div key={i}> {page.items.map(todo => ( <div key={todo.id}>{todo.text}</div> ))} </div> ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'} </button> </> ) } ``` ## Dependent Queries Run queries that depend on previous query results: ```typescript // Get user first const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }) // Then get projects (only runs if user exists) const { data: projects } = useQuery({ queryKey: ['projects', user?.id], queryFn: () => fetchProjects(user.id), enabled: !!user?.id, }) ``` ## Parallel Queries Run multiple queries simultaneously: ```typescript function Dashboard() { const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) const projects = useQuery({ queryKey: ['projects'], queryFn: fetchProjects }) const tasks = useQuery({ queryKey: ['tasks'], queryFn: fetchTasks }) // All queries run in parallel if (users.isLoading || projects.isLoading || tasks.isLoading) { return <div>Loading...</div> } return ( <div> <UserList users={users.data} /> <ProjectList projects={projects.data} /> <TaskList tasks={tasks.data} /> </div> ) } ``` ## useQueries Hook Dynamic parallel queries: ```typescript import { useQueries } from '@tanstack/react-query' function MultiTodos({ todoIds }: { todoIds: number[] }) { const results = useQueries({ queries: todoIds.map(id => ({ queryKey: ['todo', id], queryFn: () => fetchTodo(id), })), }) const isLoading = results.some(result => result.isLoading) return ( <div> {results.map((result, i) => ( <div key={i}> {result.isLoading ? 'Loading...' : result.data?.text} </div> ))} </div> ) } ``` ## Query Status ```typescript const { status, fetchStatus, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) // status can be: // - 'pending' - no data yet // - 'error' - query threw error // - 'success' - query has data // fetchStatus can be: // - 'fetching' - currently fetching // - 'paused' - wants to fetch but is paused // - 'idle' - not fetching // Derived booleans const { isLoading, // pending + fetching isError, // error status isSuccess, // success status isFetching, // currently fetching isRefetching, // fetching + has data } = useQuery(...) ``` ## Error Handling ```typescript import { useQuery, QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' // Component-level error handling const { error, isError } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, retry: 3, retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), }) if (isError) { return <div>Error: {error.message}</div> } // Error boundary for queries <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <button onClick={() => resetErrorBoundary()}>Try again</button> </div> )} > <MyComponent /> </ErrorBoundary> )} </QueryErrorResetBoundary> ``` ## React Query DevTools Debug queries and mutations visually: ```typescript import { ReactQueryDevtools } from '@tanstack/react-query-devtools' function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ) } ``` ## TypeScript Support Full TypeScript support with type inference: ```typescript interface Todo { id: number text: string done: boolean } const { data } = useQuery<Todo[], Error>({ queryKey: ['todos'], queryFn: async () => { const response = await fetch('/api/todos') return response.json() }, }) // data is typed as Todo[] | undefined ``` ## Best Practices 1. **Use query keys consistently** - Same key structure across the app 2. **Invalidate queries after mutations** - Keep data synchronized 3. **Set appropriate stale times** - Balance freshness vs. performance 4. **Use optimistic updates sparingly** - Only for simple, predictable updates 5. **Prefetch on user intent** - Improve perceived performance 6. **Handle errors gracefully** - Provide retry mechanisms 7. **Use React Query DevTools** - Debug and monitor query states 8. **Avoid overusing enabled option** - Can lead to complex query dependencies 9. **Structure query keys hierarchically** - `['todos', 'list']`, `['todos', 'detail', id]` 10. **Use TypeScript** - Catch type errors at compile time ## Common Patterns ### Authenticated Requests ```typescript const { data } = useQuery({ queryKey: ['user'], queryFn: async () => { const response = await fetch('/api/user', { headers: { 'Authorization': `Bearer ${getToken()}`, }, }) if (!response.ok) throw new Error('Failed to fetch') return response.json() }, }) ``` ### Polling ```typescript useQuery({ queryKey: ['notifications'], queryFn: fetchNotifications, refetchInterval: 5000, // Poll every 5 seconds }) ``` ### Suspense Mode ```typescript const queryClient = new QueryClient({ defaultOptions: { queries: { suspense: true, }, }, }) // Component will suspend until data is ready function Todo() { const { data } = useQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id), }) return <div>{data.text}</div> // data is always defined } ``` ## Resources - Official Docs: https://tanstack.com/query/latest - GitHub: https://github.com/TanStack/query - Examples: https://tanstack.com/query/latest/docs/react/examples - Video Course: https://ui.dev/react-query

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CaullenOmdahl/Nextjs-React-Tailwind-Assistant'

If you have feedback or need assistance with the MCP directory API, please join our Discord server