TanStack Query
TanStack Query (React Query)
Expert assistance with TanStack Query - Powerful data fetching for React.
Overview
TanStack Query manages server state in React:
- Caching: Automatic caching and background updates
- Refetching: Smart refetch strategies
- Mutations: Optimistic updates and cache invalidation
- DevTools: Built-in development tools
- TypeScript: Full TypeScript support
Installation
npm install @tanstack/react-query
npm install --save-dev @tanstack/react-query-devtools
Setup
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
cacheTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
useQuery
import { useQuery } from '@tanstack/react-query';
function CertificateList() {
const { data, isLoading, error } = useQuery({
queryKey: ['certificates'],
queryFn: () => fetch('/api/certificates').then(res => res.json()),
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(cert => (
<li key={cert.id}>{cert.subject}</li>
))}
</ul>
);
}
Query with Parameters
function CertificateDetail({ id }: { id: string }) {
const { data: certificate } = useQuery({
queryKey: ['certificate', id],
queryFn: () => fetch(`/api/certificates/${id}`).then(res => res.json()),
});
return <div>{certificate?.subject}</div>;
}
Dependent Queries
function Certificate({ id }: { id: string }) {
const { data: certificate } = useQuery({
queryKey: ['certificate', id],
queryFn: () => fetchCertificate(id),
});
// Only run if certificate exists
const { data: ca } = useQuery({
queryKey: ['ca', certificate?.caId],
queryFn: () => fetchCA(certificate.caId),
enabled: !!certificate?.caId,
});
return <div>{ca?.subject}</div>;
}
useMutation
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateCertificate() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newCert) => {
return fetch('/api/certificates', {
method: 'POST',
body: JSON.stringify(newCert),
});
},
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['certificates'] });
},
});
return (
<button
onClick={() => mutation.mutate({ subject: 'CN=example.com' })}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Creating...' : 'Create Certificate'}
</button>
);
}
Optimistic Updates
const mutation = useMutation({
mutationFn: updateCertificate,
onMutate: async (newCert) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['certificates'] });
// Snapshot previous value
const previousCerts = queryClient.getQueryData(['certificates']);
// Optimistically update
queryClient.setQueryData(['certificates'], (old) =>
old.map((cert) =>
cert.id === newCert.id ? newCert : cert
)
);
return { previousCerts };
},
onError: (err, newCert, context) => {
// Rollback on error
queryClient.setQueryData(['certificates'], context.previousCerts);
},
onSettled: () => {
// Refetch after success or error
queryClient.invalidateQueries({ queryKey: ['certificates'] });
},
});
Pagination
function CertificateList() {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['certificates', page],
queryFn: () => fetchCertificates(page),
keepPreviousData: true, // Keep old data while fetching new
});
return (
<>
<ul>
{data?.certificates.map(cert => (
<li key={cert.id}>{cert.subject}</li>
))}
</ul>
<button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
Previous
</button>
<button onClick={() => setPage(p => p + 1)} disabled={!data?.hasMore}>
Next
</button>
</>
);
}
Infinite Queries
import { useInfiniteQuery } from '@tanstack/react-query';
function InfiniteCertificates() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['certificates'],
queryFn: ({ pageParam = 1 }) => fetchCertificates(pageParam),
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
initialPageParam: 1,
});
return (
<>
{data?.pages.map((page, i) => (
<div key={i}>
{page.certificates.map(cert => (
<div key={cert.id}>{cert.subject}</div>
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
</>
);
}
Cache Invalidation
const queryClient = useQueryClient();
// Invalidate all queries
queryClient.invalidateQueries();
// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['certificates'] });
// Invalidate query with params
queryClient.invalidateQueries({ queryKey: ['certificate', '123'] });
// Invalidate all queries starting with key
queryClient.invalidateQueries({ queryKey: ['certificates'], exact: false });
// Remove query from cache
queryClient.removeQueries({ queryKey: ['certificates'] });
// Reset query to initial state
queryClient.resetQueries({ queryKey: ['certificates'] });
Manual Cache Updates
// Set query data
queryClient.setQueryData(['certificate', '123'], newCertificate);
// Update query data
queryClient.setQueryData(['certificates'], (old) =>
old.map((cert) => cert.id === '123' ? updated : cert)
);
// Get query data
const certificates = queryClient.getQueryData(['certificates']);
// Prefetch query
await queryClient.prefetchQuery({
queryKey: ['certificate', '123'],
queryFn: () => fetchCertificate('123'),
});
Best Practices
- Query Keys: Use arrays for structured keys
['certificates', { status: 'active' }] - Stale Time: Set appropriate stale time for your data
- Cache Time: Keep data in cache longer than stale time
- Optimistic Updates: Improve UX with optimistic updates
- Error Handling: Handle errors gracefully
- Invalidation: Invalidate related queries on mutations
- Pagination: Use
keepPreviousDatafor better UX - DevTools: Use DevTools for debugging
- TypeScript: Define types for query data
- Prefetching: Prefetch data for better performance
Resources
- Documentation: https://tanstack.com/query
- GitHub: https://github.com/TanStack/query
More from oriolrius/pki-manager-web
keycloak
Expert guidance for Keycloak identity and access management including realm configuration, client setup, user federation, authentication flows, role-based access control, and integration with applications. Use this when setting up authentication, configuring SSO, managing users and roles, or integrating Keycloak with applications.
60trpc
Expert guidance for tRPC (TypeScript Remote Procedure Call) including router setup, procedures, middleware, context, client configuration, and Next.js integration. Use this when building type-safe APIs, integrating tRPC with Next.js, or implementing client-server communication with full TypeScript inference.
37next.js
Expert guidance for Next.js framework including App Router, Server Components, routing, data fetching, API routes, middleware, and deployment. Use this when building Next.js applications, working with React Server Components, or implementing Next.js features.
25sqlite
Expert guidance for SQLite database with better-sqlite3 Node.js driver including database setup, queries, transactions, migrations, performance optimization, and integration with TypeScript. Use this when working with embedded databases, better-sqlite3 driver, or SQLite operations.
24backlog.md
Expert guidance for Backlog.md CLI project management tool including task creation, editing, status management, acceptance criteria, search, and board visualization. Use this when managing project tasks, creating task lists, updating task status, or organizing project work.
20oauth2
Expert guidance for OAuth 2.0 protocol including authorization flows, grant types, token management, OpenID Connect, security best practices, and implementation patterns. Use this when implementing authentication/authorization, working with OAuth providers, securing APIs, or integrating with third-party services.
12