Data Fetching

Server Actions with React Query for optimal data management

Architecture Overview

Our data fetching strategy combines Server Actions with React Query for a powerful, type-safe approach:

  1. Server Actions handle all API communication (authentication, API client setup)
  2. React Query hooks manage client-side caching, refetching, and state
  3. Centralized query keys ensure consistent cache invalidation

Fetching Strategies

hooks/guest-disruptions/use-guest-disruptions.ts
"use client";

import { useQuery } from "@tanstack/react-query";
import { getGuestDisruptions } from "@/app/actions/guest-disruptions/get-guest-disruptions";
import { queryKeys } from "@/lib/query-keys";

export function useGuestDisruptions(scheduleId: string, params?: GuestDisruptionsParams) {
  return useQuery({
    queryKey: queryKeys.guestDisruptions.list(scheduleId, params),
    queryFn: () => getGuestDisruptions(scheduleId, params),
    staleTime: 5 * 60 * 1000, // 5 min
    gcTime: 15 * 60 * 1000, // 15 min
    refetchOnWindowFocus: false,
    enabled: !!scheduleId,
    placeholderData: (previousData) => previousData, // Smooth UX
  });
}

Recommended approach - Use custom hooks that wrap Server Actions with React Query.

Benefits:

  • Automatic caching and background refetching
  • Optimistic updates and request deduplication
  • Consistent configuration across the app
  • Type-safe with full TypeScript support
app/actions/guest-disruptions/get-guest-disruptions.ts
"use server";

import { auth } from "@clerk/nextjs/server";
import { createApiClient } from "@/lib/api-client";
import { API_CONFIG } from "@/config/api";

export async function getGuestDisruptions(
  scheduleId: string,
  params?: GuestDisruptionsParams
) {
  // Get authentication token
  const { getToken } = await auth();
  const token = await getToken();

  if (!token) {
    throw new Error("Unauthorized");
  }

  // Create authenticated API client
  const apiClient = createApiClient(token);

  // Build query parameters
  const searchParams = new URLSearchParams();
  if (params?.pathwayAvailability !== undefined) {
    searchParams.append("pathwayAvailability", params.pathwayAvailability.toString());
  }
  // ... other params

  const endpoint = API_CONFIG.endpoints.guestDisruptions.list(scheduleId);
  const data = await apiClient.get(`${endpoint}?${searchParams}`).json();

  return data;
}

Server Actions handle authentication, API client setup, and error handling.

Responsibilities:

  • Clerk authentication
  • API client configuration with tokens
  • Request/response mapping
  • Error handling
components/guest-disruptions-table.tsx
"use client";

import { useGuestDisruptions } from "@/hooks/guest-disruptions/use-guest-disruptions";

export function GuestDisruptionsTable({ scheduleId }: Props) {
  const { data, isLoading, error } = useGuestDisruptions(scheduleId);

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorMessage error={error} />;

  return <DataTable data={data.disruptions} columns={columns} />;
}

Use the custom hooks directly in components for clean, declarative code.

React Query Setup

Provider Configuration

src/providers/query-provider.tsx
"use client";

import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

export function QueryProvider({ children }: { children: ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 2 * 60 * 1000, // 2 minutes
            gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime)
            retry: 1,
            refetchOnWindowFocus: false,
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      {process.env.NODE_ENV === "development" && <ReactQueryDevtools />}
    </QueryClientProvider>
  );
}

Centralized Query Keys

lib/query-keys.ts
export const queryKeys = {
  // Schedules
  all: ["schedules"] as const,
  lists: () => [...queryKeys.all, "list"] as const,
  list: () => [...queryKeys.lists()] as const,
  details: () => [...queryKeys.all, "detail"] as const,
  detail: (id: string) => [...queryKeys.details(), id] as const,

  // Guest Disruptions
  guestDisruptions: {
    all: ["guestDisruptions"] as const,
    lists: () => [...queryKeys.guestDisruptions.all, "list"] as const,
    list: (scheduleId: string, params?: Record<string, unknown>) =>
      [...queryKeys.guestDisruptions.lists(), scheduleId, params] as const,
    filterOptions: (scheduleId: string) =>
      [...queryKeys.guestDisruptions.all, "filterOptions", scheduleId] as const,
    scheduleChanges: (scheduleId: string, route: string) =>
      [
        ...queryKeys.guestDisruptions.all,
        "scheduleChanges",
        scheduleId,
        route,
      ] as const,
    impactedDates: (scheduleId: string, route: string) =>
      [
        ...queryKeys.guestDisruptions.all,
        "impactedDates",
        scheduleId,
        route,
      ] as const,
    alternativePathways: (
      scheduleId: string,
      route: string,
      params?: Record<string, unknown>
    ) =>
      [
        ...queryKeys.guestDisruptions.all,
        "alternativePathways",
        scheduleId,
        route,
        params,
      ] as const,
  },
} as const

Custom Hook Pattern

hooks/schedules/use-schedules.ts
"use client"

import { useQuery } from "@tanstack/react-query"

import { queryKeys } from "@/lib/query-keys"
import { getSchedules } from "@/app/actions/schedules/get-schedules"

export function useSchedules() {
  return useQuery({
    queryKey: queryKeys.list(),
    queryFn: getSchedules,
    // Uses default staleTime (2 min) and gcTime (10 min)
  })
}

Best Practices


Next: Learn about Routing with Next.js App Router.