Data Fetching
Learn how to fetch data from APIs using the custom hooks built into the boilerplate.
Overview
The boilerplate provides two custom hooks that wrap React Query and integrate with the HTTP client:
- useApiQuery - For GET requests (fetching data)
- useApiMutation - For POST, PUT, PATCH, DELETE requests (modifying data)
These hooks automatically use the configured httpClient with authentication, error handling, and interceptors built-in.
useApiQuery
Custom hook that combines React Query with the httpClient from the boilerplate. Perfect for fetching data from APIs.
Basic Usage
typescript
import { useApiQuery } from '@/hooks/useApi';
export function useUserListViewModel() {
const { data, isLoading, error, refetch } = useApiQuery<User[]>(
['users'], // Query key for caching
'/api/users' // Endpoint
);
return {
users: data?.data ?? [], // Access response data
isLoading,
error,
refetch,
};
}With React Query Options
typescript
const { data, isLoading } = useApiQuery<Post[]>(
['posts', 'featured'],
'/api/posts/featured',
{
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
enabled: true, // Run immediately
refetchOnWindowFocus: false,
}
);Response Structure
typescript
const { data } = useApiQuery<User[]>(['users'], '/api/users');
// data contains:
{
data: User[], // Response body
status: 200, // HTTP status code
headers: { ... } // Response headers
}useApiMutation
Custom hook for mutations (POST, PUT, PATCH, DELETE) that modify data on the server.
POST Request
typescript
import { useApiMutation } from '@/hooks/useApi';
import { useQueryClient } from '@tanstack/react-query';
export function useCreateUserViewModel() {
const queryClient = useQueryClient();
const createMutation = useApiMutation<User, CreateUserInput>(
'post',
'/api/users',
{
onSuccess: () => {
// Invalidate and refetch users list
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (error) => {
console.error('Failed to create user:', error.message);
},
}
);
const handleCreateUser = (userData: CreateUserInput) => {
createMutation.mutate(userData);
};
return {
handleCreateUser,
isCreating: createMutation.isPending,
error: createMutation.error,
};
}PUT Request with Dynamic URL
typescript
const updateMutation = useApiMutation<User, UpdateUserInput>(
'put',
(variables) => `/api/users/${variables.id}`, // Dynamic URL
{
onSuccess: (response) => {
console.log('User updated:', response.data);
queryClient.invalidateQueries({ queryKey: ['users'] });
},
}
);
// Usage
updateMutation.mutate({
id: '123',
name: 'John Updated',
email: 'john@example.com'
});DELETE Request
typescript
const deleteMutation = useApiMutation<void, string>(
'delete',
(id) => `/api/users/${id}`,
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
}
);
// Usage
const handleDelete = (userId: string) => {
deleteMutation.mutate(userId);
};PATCH Request
typescript
const patchMutation = useApiMutation<User, Partial<User>>(
'patch',
(variables) => `/api/users/${variables.id}`,
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
}
);
// Usage - update only specific fields
patchMutation.mutate({
id: '123',
status: 'active'
});Complete Example
Here's a complete ViewModel with both query and mutation:
src/presentation/viewmodels/useTaskViewModel.ts
import { useApiQuery, useApiMutation } from '@/hooks/useApi';
import { useQueryClient } from '@tanstack/react-query';
import type { Task, CreateTaskInput, UpdateTaskInput } from '@/domain/entities/task';
export function useTaskViewModel() {
const queryClient = useQueryClient();
// Fetch tasks
const { data, isLoading, error } = useApiQuery<Task[]>(
['tasks'],
'/api/tasks'
);
// Create task
const createMutation = useApiMutation<Task, CreateTaskInput>(
'post',
'/api/tasks',
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
}
);
// Update task
const updateMutation = useApiMutation<Task, UpdateTaskInput>(
'put',
(variables) => `/api/tasks/${variables.id}`,
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
}
);
// Delete task
const deleteMutation = useApiMutation<void, string>(
'delete',
(id) => `/api/tasks/${id}`,
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
}
);
return {
// Data
tasks: data?.data ?? [],
isLoading,
error,
// Actions
createTask: (task: CreateTaskInput) => createMutation.mutate(task),
updateTask: (task: UpdateTaskInput) => updateMutation.mutate(task),
deleteTask: (id: string) => deleteMutation.mutate(id),
// Status
isCreating: createMutation.isPending,
isUpdating: updateMutation.isPending,
isDeleting: deleteMutation.isPending,
};
}Using in a View
src/presentation/screens/TaskScreen.tsx
import { View, Text, FlatList, Pressable, ActivityIndicator } from 'react-native';
import { useTaskViewModel } from '../viewmodels/useTaskViewModel';
export default function TaskScreen() {
const {
tasks,
isLoading,
error,
deleteTask,
isDeleting
} = useTaskViewModel();
if (isLoading) {
return (
<View className="flex-1 justify-center items-center">
<ActivityIndicator size="large" />
</View>
);
}
if (error) {
return (
<View className="flex-1 justify-center items-center">
<Text className="text-red-500">Error: {error.message}</Text>
</View>
);
}
return (
<FlatList
data={tasks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View className="bg-white dark:bg-slate-800 p-4 mb-2 rounded-lg">
<Text className="text-lg font-semibold">{item.title}</Text>
<Text className="text-slate-600 dark:text-slate-400">
{item.description}
</Text>
<Pressable
onPress={() => deleteTask(item.id)}
disabled={isDeleting}
className="mt-2"
>
<Text className="text-red-500">
{isDeleting ? 'Deleting...' : 'Delete'}
</Text>
</Pressable>
</View>
)}
/>
);
}Type Safety
Both hooks are fully typed with TypeScript generics:
typescript
// useApiQuery<ResponseType>
const { data } = useApiQuery<User[]>(
['users'],
'/api/users'
);
// data.data is typed as User[]
// useApiMutation<ResponseType, VariablesType>
const mutation = useApiMutation<User, CreateUserInput>(
'post',
'/api/users'
);
// mutation.mutate expects CreateUserInput
// response.data is typed as UserError Handling
typescript
const { error } = useApiQuery<User[]>(['users'], '/api/users');
if (error) {
// Error is normalized by httpClient
const err = error as Error & { status?: number };
if (err.status === 404) {
return <Text>Users not found</Text>;
}
if (err.status === 401) {
// Redirect to login
router.push('/login');
}
return <Text>Error: {err.message}</Text>;
}The httpClient automatically handles authentication tokens, retries, and error normalization!