Stop Reinventing the Wheel: Robust API Handling in React
Battle-tested patterns for data fetching, caching, error handling, and type safety in production React apps.

Early in my career, I built custom hooks for every API call. Hours spent debugging race conditions, stale data, and cache invalidation.
Then I learned a hard lesson: for complex apps, use battle-tested libraries.
Here's my approach now:
1. Use TanStack Query or SWR
Stop building your own data fetching solution. These libraries handle caching, refetching, optimistic updates, and request deduplication out of the box. They've solved problems you don't even know you have yet.
import { useQuery } from '@tanstack/react-query';
function useUser(id: string) {
return useQuery({
queryKey: ['user', id],
queryFn: () => axios.get(`/api/users/${id}`).then(res => res.data),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}The query key ['user', id] becomes your cache key. Same key = same cache. No duplicate requests across components.
TanStack Query also handles window refocus refetching, background updates, and pagination out of the box. These features would take weeks to implement correctly from scratch.
2. Axios for HTTP Clients
Better error handling, interceptors for auth tokens, request/response transformations. Fetch is great, but Axios saves you from writing boilerplate.
const api = axios.create({
baseURL: '/api',
timeout: 10000,
});
api.interceptors.request.use((config) => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
redirectToLogin();
}
return Promise.reject(error);
}
);Centralized auth handling. Centralized error interception. No more try/catch blocks scattered across every component.
3. Always Handle Cleanup
Even with libraries, implement AbortController to cancel in-flight requests on unmount. Memory leaks are silent killers.
function useSearch(query: string) {
return useQuery({
queryKey: ['search', query],
queryFn: async ({ signal }) => {
const { data } = await api.get('/search', {
params: { q: query },
signal, // TanStack Query passes AbortSignal automatically
});
return data;
},
enabled: query.length > 0,
});
}TanStack Query passes the AbortSignal to your query function. Use it. When the component unmounts or the query key changes, the previous request is automatically cancelled.
4. Centralize Error Handling
Use error boundaries and global error interceptors. Users shouldn't see cryptic 500 errors or blank screens.
// Global error handler for your API layer
function handleApiError(error: unknown): never {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const message = error.response?.data?.message ?? 'Something went wrong';
switch (status) {
case 401: throw new AuthError(message);
case 403: throw new ForbiddenError(message);
case 404: throw new NotFoundError(message);
default: throw new ApiError(message, status);
}
}
throw new NetworkError('Unable to reach the server');
}
// React Error Boundary catches what slips through
<ErrorBoundary fallback={<ErrorFallback />}>
<App />
</ErrorBoundary>5. Type Safety with Zod or TypeScript
Validate API responses at runtime. The backend will break your contract eventually — be ready for it.
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'viewer']),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const { data } = await api.get(`/users/${id}`);
return UserSchema.parse(data); // Throws if shape doesn't match
}If the backend silently changes email from a string to an object, your app doesn't silently break. It fails fast with a clear error message.
The Bottom Line
The real wisdom isn't knowing how to build everything from scratch. It's knowing when to leverage proven solutions so you can focus on building features that matter to your users.
Your time is valuable. Spend it solving business problems, not reinventing solved infrastructure.
Need help architecting your React data layer? Get in touch.