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
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
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
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
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
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