๐ฃ The Hook
The first 3 lines decide if someone stays .
"I wasted months rewriting the same fetch logic in every React project. One weekend, I built a hook that eliminated 100% of that boilerplate. Here it is, free and open-source."
You know the drill. You start a new React component, and before you write a single line of UI, you're already typing:
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => { ...
It's tedious. It clutters your components. And if you forget to clean up with an AbortController? You've got memory leaks and that dreaded "Can't perform a React state update on an unmounted component" warning.
I finally got tired of it and built useFetch : a single, production-ready custom hook that handles fetching, caching, loading states, and request cancellation with full TypeScript support.
Today, I'm giving you the full source code and explaining the key design decisions that make it "production-ready."
Why Not Just Use React Query or SWR?
This is the first question I get. Libraries like React Query and SWR are fantastic, but they are heavy.
Zero Dependencies: My hook is under 3KB. It doesn't bring an entire state management layer into your bundle.
Full Control: You own the code. It's a single file you can drop into your src/hooks folder and customize to your heart's content.
Learning Experience: Understanding how these hooks work under the hood makes you a better engineer.
๐ The Code: A Step-by-Step Breakdown
Let's build this. We need it to be type-safe, handle race conditions, and include a smart caching layer.
- TypeScript First: Defining the Contract Always start with the types. This makes the actual implementation trivial.
typescript
interface UseFetchOptions {
initialData?: T | null;
cacheDuration?: number; // Cache time in ms
skip?: boolean; // Don't fetch automatically
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
deps?: unknown[]; // Re-fetch dependencies
}
interface UseFetchReturn {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => Promise;
clearCache: () => void;
}
- The Singleton Cache (The Secret Sauce) To avoid fetching the same user profile 50 times across different components, we need a global cache. We use a simple JavaScript object with a TTL (Time To Live) to ensure data freshness.
typescript
// cacheManager.ts
const cache = new Map();
export const cacheManager = {
get(key: string, ttl: number): T | null {
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > ttl) {
cache.delete(key);
return null;
}
return entry.data as T;
},
set(key: string, data: unknown): void {
cache.set(key, { data, timestamp: Date.now() });
},
clear(key: string): void {
cache.delete(key);
}
};
- The Hook Implementation: Taming Race Conditions The critical part is the AbortController. Without it, if a user clicks "User 1" then quickly clicks "User 2", a slow API response for User 1 might arrive after User 2's data, overriding the state with stale information .
tsx
import { useEffect, useState, useCallback, useRef } from 'react';
export function useFetch(
url: string | null,
options: UseFetchOptions = {}
): UseFetchReturn {
const { cacheDuration = 300000, skip = false, deps = [] } = options;
const [data, setData] = useState(options.initialData || null);
const [loading, setLoading] = useState(!skip);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
const fetchData = useCallback(async () => {
if (!url) return;
// 1. Check Cache
const cached = cacheManager.get<T>(url, cacheDuration);
if (cached) {
setData(cached);
setLoading(false);
return;
}
// 2. Cancel previous call
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
try {
setLoading(true);
const response = await fetch(url, { signal: abortControllerRef.current.signal });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const result = await response.json();
cacheManager.set(url, result);
setData(result);
options.onSuccess?.(result);
} catch (err) {
// Ignore abort errors - they are intentional
if (err instanceof Error && err.name !== 'AbortError') {
setError(err);
options.onError?.(err);
}
} finally {
setLoading(false);
}
}, [url, cacheDuration]);
useEffect(() => {
if (skip) return;
fetchData();
// Cleanup: Abort request on unmount
return () => abortControllerRef.current?.abort();
}, [fetchData, skip, ...deps]);
const refetch = useCallback(() => {
cacheManager.clear(url!);
fetchData();
}, [fetchData, url]);
return { data, loading, error, refetch, clearCache: () => cacheManager.clear(url!) };
}
๐ ๏ธ How to Use It (It's Stupidly Simple)
Using the hook is cleaner than your current useEffect soup.
tsx
interface User {
id: number;
name: string;
}
const UserProfile = ({ userId }: { userId: number }) => {
const { data: user, loading, error } = useFetch(
https://api.example.com/users/${userId},
{ deps: [userId] } // Auto re-fetch when userId changes
);
if (loading) return ;
if (error) return ;
return
Welcome back, {user?.name}!;};
๐ Why This Will Save You Hours
No More Memory Leaks: The AbortController cleanup ensures you never update an unmounted component.
Instant Back-Navigation: Caching means data appears instantly when revisiting a page. No loading spinners.
Type Safety: Generics ensure data is fully typed as User or Product[]. No more any.
๐ฆ Full Production-Ready Code
I've prepared a complete, split-source GitHub repository with:
โ Full TypeScript support
โ Comprehensive Unit Tests
โ Advanced Caching with TTL
โ GitHub Actions CI/CD setup
โ A beautiful README ready for npm
๐ GitHub Repo: use-fetch-cache
Don't forget to drop a โญ if you find it useful!
๐ฌ Final Thoughts
Building your own tools is the best way to master a framework. You don't always need a 20kB third-party library to solve a 2kB problem. By writing useFetch, you've taken control of your data layer and reduced your bundle size simultaneously.
Have you built a custom hook that changed your workflow? Drop a comment belowโI'd love to see what you're building!
Top comments (0)