LogoStarterkitpro
Features

Tanstack React Query

Use React Query selectively for client-side components in Next.js

Optional Feature

StarterKitPro keeps the default setup lean. React Query is not included out-of-the-box but can be easily added in under a minute if you want to use it.

Using React Query Selectively in Next.js

Core Guidance:

  1. Fetch Data: Use Server Components (Next.js standard).
  2. Mutate Data: Use Server Actions (Next.js standard).
  3. React Query Use Case: Primarily for client-side caching when a component needs to refetch the same data repeatedly (e.g., after filtering/sorting), preventing excess API calls.

Complexity Alert

Avoid React Query for initial loads (prefer Server Components) or mutations (prefer Server Actions) to prevent complexity and potential hydration issues.

Usually Not Needed

Native Next.js features (Server Components, Actions, API Routes) cover most needs. Use React Query only when client-side caching offers a clear benefit.

Pattern (When using React Query for Caching):

  1. Use React Query in client components for caching subsequent API calls.
  2. Use Server Actions for mutations.
  3. After a Server Action, invalidate the React Query cache (useClearCache).

Setup

Let's set up React Query step by step:

1. Installation

Terminal
npm install @tanstack/react-query

2. Query Client Provider

Create a provider to configure the QueryClient.

providers/query-client-provider.tsx
"use client";
import {
  QueryClientProvider,
  QueryClient,
  QueryCache,
} from "@tanstack/react-query";
 
const queryClient = new QueryClient({
  queryCache: new QueryCache({}),
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      retry: false,
    },
  },
});
 
export const CustomQueryClientProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

3. Wrap Your Application

Wrap your root layout with the CustomQueryClientProvider.

app/layout.tsx
import { CustomQueryClientProvider } from "@/providers/query-client-provider"; 
import { SessionProvider } from "next-auth/react"; // If using session
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={`${font.className} antialiased`}>
        <SessionProvider>
          <CustomQueryClientProvider>
            {children}
          </CustomQueryClientProvider>
        </SessionProvider>
      </body>
    </html>
  );
}

Fetching Data with API Routes

1. Custom Hooks for Queries

Encapsulate fetching logic and cache invalidation in custom hooks.

hooks/use-cache.ts
import { useQuery, useQueryClient } from "@tanstack/react-query";
import apiClient from "@/lib/api-client"; // Your API client
 
// --- Cache Invalidation Hook (Utility) ---
// Returns a function to invalidate specified query keys.
export function useClearCache(cacheKeys: (string | unknown[])[]) {
  const queryClient = useQueryClient();
  const clearCache = () =>
    cacheKeys.forEach((key) =>
      queryClient.invalidateQueries({
        queryKey: Array.isArray(key) ? key : [key],
      })
    );
  return [clearCache];
}
 
// --- Example Data Fetching Hooks ---
 
// Fetch all tasks via API
export function useFetchTasksAPI() {
  return useQuery({
    queryKey: ["tasksAPI"],
    queryFn: () => apiClient.get("/api/tasks"),
  });
}
 
// Fetch tasks for a specific user via API (conditionally)
export function useFetchTasksAPIByUserId(userId: number | null) {
  return useQuery({
    queryKey: ["tasksAPI", userId], // Parameterized query key
    queryFn: () => apiClient.get(`/api/tasks?userId=${userId}`),
    enabled: !!userId, // Only run if userId exists
  });
}

Use parameterized query keys (e.g., ["tasksAPI", userId]) for dynamic queries.

2. Using Hooks in Components

Call your custom fetching hook within client components.

// --- Example ---
import { useFetchTasksAPI } from "@/hooks/use-cache";
import { Skeleton } from "@/components/ui/skeleton";
 
export function TaskListComponent() {
  const { data: tasks = [], isLoading, error } = useFetchTasksAPI();
 
  if (isLoading) return <Skeleton className="h-20 w-full" />;
  if (error) return <div>Error: {error.message}</div>;
 
  return (
    <ul>
      {tasks.map((task: any) => (
        <li key={task.id}>{task.title}</li>
      ))}
    </ul>
  );
}

Invalidating Cache After Action

Use the useClearCache hook to invalidate relevant query keys after an action (e.g., Server Action mutation) modifies data.

// --- Example ---
import { useClearCache } from "@/hooks/use-cache";
import { Button } from "@/components/ui/button";
 
export function UpdateTaskButton({ taskId }: { taskId: string }) {
  // Get function to invalidate the general tasks list cache
  const [clearTasksCache] = useClearCache(["tasksAPI"]);
 
  // Optionally, invalidate a specific user's task list
  // const [clearUserTasksCache] = useClearCache(["tasksAPI", userId]);
 
  const handleUpdate = async () => {
    try {
      // Perform the action (e.g., call Server Action)
      // ... action logic ...
 
      // Invalidate cache(s)
      clearTasksCache();
      // clearUserTasksCache(); // If needed
    } catch (error) {
      toast.error("Action failed.");
    }
  };
 
  return <Button onClick={handleUpdate}>Mark Complete</Button>;
}

Invalidating keys like ["tasksAPI"] ensures components using useFetchTasksAPI refetch data.

Using Server Actions for Fetching

Alternatively, call Server Actions directly within queryFn if needed, using distinct query keys.

hooks/use-cache.ts (Server Action Fetch Example)
// --- Example ---
import { useQuery } from "@tanstack/react-query";
import { fetchTasks as fetchTasksAction } from "@/actions/task-actions"; // Server Action
 
export function useFetchTasksSA() {
  return useQuery({
    queryKey: ["tasksSA"], // Distinct key
    queryFn: () => fetchTasksAction(),
  });
}

Best Practices Summary

  1. Fetch: Use API Routes + React Query (useQuery).
  2. Mutate: Use Server Actions.
  3. Invalidate: After Server Action mutation, use useClearCache to invalidate relevant API Route query keys.
  4. Custom Hooks: Encapsulate query logic.
  5. Query Keys: Use specific, array-based keys.
  6. Error Handling: Handle errors in components or onError.

Conclusion

Pairing React Query with API Routes for fetching and Server Actions for mutations provides a robust pattern for managing client-side data in Next.js. Use useClearCache to bridge the gap, ensuring data freshness after mutations.

On this page