Advanced Patterns
Advanced React hooks configuration, HTTP methods, and cache management
Advanced Patterns
Learn advanced techniques for configuring and optimizing your React hooks with hypequery.
HTTP Method Configuration
By default, useQuery and useMutation issue GET requests. Override HTTP methods per query when you need POST, PUT, or other verbs:
import { createHooks } from '@hypequery/react'; import { InferApiType } from '@hypequery/serve'; import type { api } from '@/analytics/queries'; type Api = InferApiType<typeof api>; export const { useQuery, useMutation } = createHooks<Api>({ baseUrl: '/api', config: { weeklyRevenue: { method: 'GET' }, // Read-only queries tripStats: { method: 'GET' }, rebuildMetrics: { method: 'POST' }, // Write operations updateMetric: { method: 'PUT' }, }, });
When to Use Different Methods
- GET: Read-only queries, safe to cache
- POST: Write operations, mutations, or queries with large inputs
- PUT: Updates to specific resources
- DELETE: Removal operations
Auto-Config from Server
Instead of manually maintaining HTTP method configuration, let the server tell the client which methods to use.
Option A: Shared Server Module (Next.js / Remix)
When your React code can import the server bundle (e.g., Next.js App Router), pass the api directly:
// lib/analytics.ts import { createHooks } from '@hypequery/react'; import { InferApiType } from '@hypequery/serve'; import { api } from '@/analytics/queries'; type Api = InferApiType<typeof api>; export const { useQuery, useMutation } = createHooks<Api>({ baseUrl: '/api/hypequery', api, // ✅ Method metadata extracted automatically });
The api object contains method metadata from your route definitions. This keeps client and server configuration in sync automatically.
Passing api (or manifest: api.manifest()) is required when you use semantic metrics/datasets endpoints with useMetric/useDataset. Their POST paths differ from their map keys, so the hooks need the resolved routes. extractClientConfig(api) in Option B includes these routes too.
Option B: Config Endpoint (SPAs / Vite)
If your frontend can't import the server module, expose a configuration endpoint:
1. Create a config endpoint:
// app/api/hypequery-config/route.ts import { extractClientConfig } from '@hypequery/serve'; import { api } from '@/analytics/queries'; export function GET() { return Response.json(extractClientConfig(api)); }
2. Load config at runtime:
// lib/analytics.ts import { createHooks } from '@hypequery/react'; import { InferApiType } from '@hypequery/serve'; import type { api } from '@/analytics/queries'; type Api = InferApiType<typeof api>; let hooksPromise: Promise<ReturnType<typeof createHooks<Api>>> | null = null; export function getHypequeryHooks() { if (!hooksPromise) { hooksPromise = fetch('/api/hypequery-config') .then((res) => res.json()) .then((config) => createHooks<Api>({ baseUrl: '/api/hypequery', config }) ); } return hooksPromise; }
3. Initialize in your app:
// app/providers.tsx import { getHypequeryHooks } from '@/lib/analytics'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; const queryClient = new QueryClient(); export function AppProviders({ children }: { children: React.ReactNode }) { const [hooks, setHooks] = useState<any>(null); useEffect(() => { getHypequeryHooks().then(setHooks); }, []); if (!hooks) return <div>Loading...</div>; return ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> ); }
Query Client Access
Access the TanStack Query client directly for advanced cache management:
import { useQueryClient } from '@tanstack/react-query'; function MetricsPanel() { const queryClient = useQueryClient(); const { data } = useQuery('metrics', {}); const handleRefresh = () => { // Invalidate specific queries queryClient.invalidateQueries({ queryKey: ['metrics'] }); // Or clear entire cache queryClient.clear(); }; return ( <div> <div>{data?.total}</div> <button onClick={handleRefresh}>Refresh</button> </div> ); }
Cache Invalidation
Invalidate After Mutations
Automatically refresh related queries when data changes:
import { useMutation, useQuery } from '@/lib/analytics'; import { useQueryClient } from '@tanstack/react-query'; function MetricsManager() { const queryClient = useQueryClient(); const { data: metrics } = useQuery('metrics', {}); const rebuild = useMutation('rebuildMetrics', { onSuccess: () => { // Invalidate related queries queryClient.invalidateQueries({ queryKey: ['metrics'] }); queryClient.invalidateQueries({ queryKey: ['weeklyRevenue'] }); }, }); return ( <button onClick={() => rebuild.mutate({ force: true })}> Rebuild </button> ); }
Prefetching
Preload data before it's needed:
import { useQueryClient } from '@tanstack/react-query'; function Dashboard() { const queryClient = useQueryClient(); useEffect(() => { // Prefetch on mount queryClient.prefetchQuery({ queryKey: ['weeklyRevenue', { startDate: '2025-01-01' }], queryFn: () => fetch('/api/weeklyRevenue?startDate=2025-01-01') .then(res => res.json()), }); }, []); // ... }
Query Keys
hypequery hooks use a fixed query-key structure: ['hypequery', name], or ['hypequery', name, input] when the query has input. Dataset hooks key on their resolved name (dataset:<name>). Use these keys with TanStack's cache APIs:
// Invalidate one query (must match name + input). queryClient.invalidateQueries({ queryKey: ['hypequery', 'weeklyRevenue'] }); // Invalidate every hypequery cache entry by prefix. queryClient.invalidateQueries({ queryKey: ['hypequery'] });
The key structure is not configurable — relying on the stable 'hypequery' prefix lets you target hypequery caches without colliding with other TanStack queries.
Auth headers and token refresh
Add auth tokens with the headers option. It accepts a static object or a (optionally async) function that runs per request, so it can supply a fresh short-lived token. Pair it with onUnauthorized to refresh credentials and retry once on a 401:
export const { useQuery, useMutation } = createHooks<Api>({ baseUrl: '/api', headers: async () => ({ Authorization: `Bearer ${await getAuthToken()}`, }), onUnauthorized: async () => { // Runs on a 401; refresh the token, then the request retries once. await refreshSession(); }, });
If you need full control over the request, override the fetch implementation with fetchFn:
export const { useQuery, useMutation } = createHooks<Api>({ baseUrl: '/api', fetchFn: async (url, options) => { const token = getAuthToken(); return fetch(url, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${token}`, }, }); }, });
Suspense Mode
TanStack Query v5 removed the per-call suspense: true flag in favor of dedicated useSuspenseQuery/useSuspenseInfiniteQuery hooks. hypequery's hooks wrap the non-suspense hooks, so handle loading with isLoading (shown elsewhere on this page). If you need Suspense, read the endpoint with your own useSuspenseQuery call against the same route.
Use React Suspense for declarative loading states:
import { Suspense } from 'react'; import { useSuspenseQuery } from '@tanstack/react-query'; function MetricsChart() { // Throws a promise while loading, so the parent <Suspense> shows its fallback. const { data } = useSuspenseQuery({ queryKey: ['hypequery', 'weeklyRevenue', { startDate: '2025-01-01' }], queryFn: () => fetch('/api/weeklyRevenue?startDate=2025-01-01').then((res) => res.json()), }); return <div>Total: ${data.total}</div>; } function Dashboard() { return ( <Suspense fallback={<div>Loading metrics...</div>}> <MetricsChart /> </Suspense> ); }
Error Boundaries
Handle errors declaratively with error boundaries:
import { ErrorBoundary } from 'react-error-boundary'; function Dashboard() { return ( <ErrorBoundary fallback={<div>Failed to load metrics</div>} onError={(error) => console.error('Metrics error:', error)} > <MetricsPanel /> </ErrorBoundary> ); }
Advanced TanStack Query Options
All TanStack Query options are supported:
const { data, refetch, isFetching } = useQuery( 'weeklyRevenue', { startDate: '2025-01-01' }, { // Caching staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 10 * 60 * 1000, // 10 minutes (was `cacheTime` before TanStack v5) // Refetching refetchOnMount: true, refetchOnWindowFocus: false, refetchInterval: 30000, // Poll every 30s // Conditional fetching enabled: isAuthenticated, // Retries retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), } );
Per-query onSuccess/onError callbacks were removed from useQuery in TanStack Query v5. Use useMutation callbacks for writes, or subscribe via the global QueryCache if you need query-level side effects.
Background Refetching
Keep data fresh with background updates:
function LiveMetrics() { const { data, dataUpdatedAt } = useQuery( 'liveMetrics', {}, { staleTime: 0, // Always consider stale refetchInterval: 5000, // Refetch every 5 seconds refetchIntervalInBackground: true, // Continue even when tab is hidden } ); return ( <div> <div>Active Users: {data?.activeUsers}</div> <div className="text-xs text-gray-500"> Updated: {new Date(dataUpdatedAt).toLocaleTimeString()} </div> </div> ); }
Parallel Queries
Execute multiple queries efficiently:
function Dashboard() { const revenue = useQuery('weeklyRevenue', { startDate: '2025-01-01' }); const users = useQuery('activeUsers', { limit: 100 }); const metrics = useQuery('systemMetrics', {}); // All three queries run in parallel if (revenue.isLoading || users.isLoading || metrics.isLoading) { return <div>Loading...</div>; } return ( <div> <RevenueChart data={revenue.data} /> <UserList data={users.data} /> <MetricsPanel data={metrics.data} /> </div> ); }
Infinite Queries
hypequery hooks include offset-paginated useInfiniteQuery, useInfiniteMetric, and useInfiniteDataset. You don't wire up getNextPageParam yourself — the hooks advance pages using the meta.pagination the server returns (requested automatically via the x-include-meta header). Set limit as the page size; offset, if provided, is the starting offset.
import { useInfiniteDataset } from '@/lib/analytics'; function InfiniteOrders() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteDataset('orders', { dimensions: ['country', 'status'], measures: ['revenue'], limit: 20, // page size }); return ( <div> {data?.pages.map((page, i) => ( <div key={i}> {/* Each page is the endpoint response: `{ data, meta }`. */} {page.data.map((row, j) => ( <div key={j}>{row.country}: {row.revenue}</div> ))} </div> ))} {hasNextPage && ( <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}> {isFetchingNextPage ? 'Loading...' : 'Load More'} </button> )} </div> ); }
useInfiniteMetric works the same way for a named metric, and useInfiniteQuery for a plain query whose endpoint returns meta.pagination.