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.

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()),
    });
  }, []);

  // ...
}

Custom Query Keys

By default, query keys are [queryName, input]. Override for advanced cache control:

export const { useQuery, useMutation } = createHooks<Api>({
  baseUrl: '/api',
  getQueryKey: (name, input) => {
    // Custom key structure
    return ['analytics', name, input];
  },
});

This is useful when sharing cache with non-hypequery queries or integrating with existing TanStack Query setups.

Request Interceptors

Modify requests before they’re sent (for auth tokens, headers, etc.):

export const { useQuery, useMutation } = createHooks<Api>({
  baseUrl: '/api',
  fetch: async (url, options) => {
    // Add auth token
    const token = getAuthToken();

    return fetch(url, {
      ...options,
      headers: {
        ...options?.headers,
        Authorization: `Bearer ${token}`,
      },
    });
  },
});

Suspense Mode

Use React Suspense for declarative loading states:

import { Suspense } from 'react';

function MetricsChart() {
  // Throws promise while loading
  const { data } = useQuery('weeklyRevenue',
    { startDate: '2025-01-01' },
    { suspense: true }
  );

  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
    cacheTime: 10 * 60 * 1000, // 10 minutes

    // 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),

    // Callbacks
    onSuccess: (data) => console.log('Data loaded:', data),
    onError: (error) => console.error('Query failed:', error),
  }
);

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

For paginated data that loads more as you scroll:

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

function InfiniteUserList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['users', 'infinite'],
    queryFn: async ({ pageParam = 0 }) => {
      const res = await fetch(
        `/api/users?offset=${pageParam}&limit=20`
      );
      return res.json();
    },
    getNextPageParam: (lastPage, pages) => {
      if (lastPage.length < 20) return undefined;
      return pages.length * 20;
    },
  });

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

Next Steps

Continue: Standalone Usage - Using the query builder without serve

Or explore: