Skip to content

Fetching data

We use TanStack Query (React Query) for all data fetching. React Query handles caching, loading states, error handling, and refetching automatically.

All data fetching functions are Next.js server functions (previously server actions) located in src/actions/ or within feature directories at src/features/*/actions/. Server functions run on the server and use the 'use server' directive. See the Next.js Server Functions documentation for more details.

How server functions work:

  • Next.js automatically creates API endpoints for each server function
  • You call them like regular functions from client components
  • TypeScript provides full type safety across the client-server boundary
  • No need to manually create API routes or handle serialization
'use server';
import prisma from '@/lib/prisma';
import { getAuthenticatedUserEmail } from '@/utils/auth';
export const getUploadedImagesForUser = async () => {
const email = await getAuthenticatedUserEmail();
if (!email) return [];
return prisma.uploadedImage.findMany({
where: { user: { email } },
});
};

Query hooks are organized in src/query/ for shared queries, or within feature directories at src/features/*/hooks/ for feature-specific queries.

Each hook should export the query key as a constant (for invalidation) and use useQuery with the server function as queryFn:

import { getUploadedImagesForUser } from '@/actions/uploadedImage';
import { useQuery } from '@tanstack/react-query';
export const UPLOADED_IMAGES_QUERY_KEY = 'uploadedImages';
export const useUploadedImages = () => {
const query = useQuery({
queryKey: [UPLOADED_IMAGES_QUERY_KEY],
queryFn: async () => getUploadedImagesForUser(),
staleTime: Infinity,
});
return { ...query, uploadedImages: query.data };
};

Use enabled to conditionally fetch data:

import { useQuery } from '@tanstack/react-query';
import { getDocumentForUser } from '@/actions/document';
export const useSelectedDocument = (documentId?: string) => {
const query = useQuery({
queryKey: ['document', 'selected', documentId],
queryFn: async () => {
if (!documentId) return null;
return getDocumentForUser(documentId);
},
enabled: Boolean(documentId),
staleTime: Infinity,
});
return { document: query.data, isLoading: query.isLoading };
};

Invalidate queries when data changes on the server. This ensures React Query refetches the data and components stay in sync.

When to invalidate:

  • After creating, updating, or deleting data
  • After user actions that modify server state (e.g., updating preferences, connecting accounts)
  • When data changes outside of React Query’s cache (e.g., server-side mutations)

Get queryClient using useQueryClient() and call invalidateQueries with the query key:

import { useQueryClient, useMutation } from '@tanstack/react-query';
import { USER_INFO_QUERY_KEY } from '@/query/user';
import { updateUserPreferences } from '@/actions/user';
const MyComponent = () => {
const queryClient = useQueryClient();
const updateMutation = useMutation({
mutationFn: updateUserPreferences,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [USER_INFO_QUERY_KEY] });
},
});
return <button onClick={() => updateMutation.mutate()}>Update</button>;
};