React Query with TypeScript

A comprehensive implementation of React Query with TypeScript, custom hooks, and error handling


Modern data fetching with React Query and TypeScript, including custom hooks, optimistic updates, and error handling.

Types

types.ts
type User = {
  id: string;
  name: string;
  email: string;
  role: 'USER' | 'ADMIN';
};
 
type CreateUserInput = {
  name: string;
  email: string;
  role?: 'USER' | 'ADMIN';
};
 
type PaginatedResponse<T> = {
  data: T[];
  pagination: {
    total: number;
    page: number;
    limit: number;
    totalPages: number;
  };
};

API Functions

api.ts
const fetchUsers = async (page: number = 1, limit: number = 10) => {
  const { data } = await axios.get<PaginatedResponse<User>>(
    `/api/users?page=${page}&limit=${limit}`
  );
  return data;
};
 
const createUser = async (userData: CreateUserInput) => {
  const { data } = await axios.post<User>('/api/users', userData);
  return data;
};

Custom Hooks

hooks.tsx
export function useUsers(page: number = 1, limit: number = 10) {
  return useQuery({
    queryKey: ['users', page, limit],
    queryFn: () => fetchUsers(page, limit),
    keepPreviousData: true,
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}
 
export function useCreateUser() {
  const queryClient = useQueryClient();
 
  return useMutation({
    mutationFn: createUser,
    onMutate: async (newUser) => {
      // Cancel any outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['users'] });
 
      // Snapshot the previous value
      const previousUsers = queryClient.getQueryData<PaginatedResponse<User>>(['users']);
 
      // Optimistically update to the new value
      if (previousUsers) {
        queryClient.setQueryData<PaginatedResponse<User>>(['users'], {
          ...previousUsers,
          data: [
            {
              id: 'temp-id',
              ...newUser,
              role: newUser.role || 'USER',
            },
            ...previousUsers.data,
          ],
        });
      }
 
      return { previousUsers };
    },
    onError: (err, newUser, context) => {
      // Rollback to the previous value
      if (context?.previousUsers) {
        queryClient.setQueryData(['users'], context.previousUsers);
      }
    },
    onSettled: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

Usage Example

UsersList.tsx
function UsersList() {
  const [page, setPage] = useState(1);
  const { data, isLoading, isError, error } = useUsers(page);
  const createUserMutation = useCreateUser();
 
  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error: {error.message}</div>;
 
  return (
    <div>
      <div className="grid gap-4">
        {data.data.map((user) => (
          <div key={user.id} className="p-4 border rounded">
            <h3>{user.name}</h3>
            <p>{user.email}</p>
            <span className="text-sm text-gray-500">{user.role}</span>
          </div>
        ))}
      </div>
 
      <div className="mt-4 flex justify-between">
        <button
          onClick={() => setPage((p) => Math.max(1, p - 1))}
          disabled={page === 1}
        >
          Previous
        </button>
        <span>Page {page} of {data.pagination.totalPages}</span>
        <button
          onClick={() => setPage((p) => Math.min(data.pagination.totalPages, p + 1))}
          disabled={page === data.pagination.totalPages}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Imports

imports.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

Features:

  • Type-safe data fetching with TypeScript
  • Custom hooks for reusability
  • Optimistic updates
  • Error handling
  • Pagination support
  • Query caching and invalidation
  • Loading and error states