Custom React Hook Fetch Cache Data: A Complete Guide

Build production-ready custom React hooks for data fetching with caching. Master implementation patterns, cache invalidation strategies, and performance best practices.

Why Custom Fetch Hooks Matter

The evolution of React data fetching has followed a predictable trajectory that most development teams experience. Early React applications typically handled data fetching directly in components using lifecycle methods, quickly realizing that this approach led to significant code duplication.

A custom fetch hook addresses these pain points by centralizing all data-related concerns into a single, reusable abstraction. When you create a hook like useFetch, you're not just encapsulating the fetch call itself--you're building a contract that ensures consistent behavior across every component that needs data.

Beyond code organization, custom fetch hooks enable sophisticated caching strategies that would be impractical to implement on a per-component basis. The result is cleaner component code that focuses on presentation rather than data logistics.

In modern web development, data fetching is one of the most critical operations that directly impacts user experience and application performance. Every time a user interacts with your application--whether they're viewing a product catalog, loading user profiles, or fetching dashboard metrics--you're making network requests that determine how responsive and fluid your application feels. While React's component model provides excellent mechanisms for managing UI state, the same cannot be said for server state--the data that lives on your backend and needs to be synchronized with your frontend.

For teams building Next.js applications where performance and SEO are built-in requirements, or complex single-page applications with multiple data dependencies, understanding how to implement effective caching in custom hooks will make applications faster, more reliable, and significantly easier to maintain.

Core Capabilities

Essential features every production-ready fetch hook should include

Request Deduplication

Prevent redundant network requests when multiple components request the same data simultaneously.

Caching Layer

Store fetched data for immediate access on subsequent requests without network overhead.

Error Handling

Comprehensive error states that components can consume for meaningful user feedback.

Loading States

Track request progress to enable loading indicators and skeleton screens.

AbortController

Cancel pending requests when components unmount to prevent memory leaks.

Retry Logic

Automatic retry for transient failures with configurable attempts and backoff.

Basic Fetch Hook Implementation
1function useFetch<T>(url: string) {2 const [data, setData] = useState<T | null>(null);3 const [loading, setLoading] = useState(true);4 const [error, setError] = useState<Error | null>(null);5 6 useEffect(() => {7 async function fetchData() {8 try {9 setLoading(true);10 const response = await fetch(url);11 if (!response.ok) {12 throw new Error(`HTTP error! status: ${response.status}`);13 }14 const json = await response.json();15 setData(json);16 setError(null);17 } catch (e) {18 setError(e instanceof Error ? e : new Error('An error occurred'));19 setData(null);20 } finally {21 setLoading(false);22 }23 }24 25 fetchData();26 }, [url]);27 28 return { data, loading, error };29}

Cache Invalidation Strategies

Perhaps no aspect of caching is more critical--and more frequently mishandled--than cache invalidation. Caching is easy: you store data when you fetch it and return the stored data on subsequent requests. Invalidation requires understanding when cached data might be stale and deciding what to do about it.

Time-Based Invalidation

The simplest staleness indicator is time. Data fetched at 3:00 PM should be considered potentially stale by 3:01 PM if the underlying information changes frequently:

const STALE_THRESHOLD = 5 * 60 * 1000; // 5 minutes

function isDataStale(timestamp: number): boolean {
 return Date.now() - timestamp > STALE_THRESHOLD;
}

Tag-Based Invalidation

For complex data relationships, tag-based invalidation provides flexibility. When a mutation affects multiple resources, you invalidate by tag rather than by specific URL:

const tagIndex = new Map<string, Set<string>>();

function invalidateTags(...tags: string[]) {
 for (const tag of tags) {
 const urls = tagIndex.get(tag);
 if (urls) {
 for (const url of urls) {
 cache.delete(url);
 }
 tagIndex.delete(tag);
 }
 }
}

Optimistic Updates

The optimistic update pattern combines immediate UI updates with potential cache invalidation. When a user performs an action, the UI is immediately updated while the actual API request happens in the background. This pattern is commonly used in libraries like TanStack Query and provides instant user feedback while ensuring data consistency.

Understanding cache invalidation requires understanding the different ways data can become stale. The simplest case is time-based staleness handled through time-to-live (TTL) settings. More complex scenarios involve data relationships and mutations--when a user updates their profile, any cached profile data becomes stale, along with cached lists that include their profile and activity feeds that show their actions.

Stale-While-Revalidate Pattern
1function useStaleFetch<T>(url: string, staleTime = 60000) {2 const [data, setData] = useState<T | null>(null);3 const [isStale, setIsStale] = useState(false);4 const cacheRef = useRef<Map<string, CacheEntry<T>>>(5 new Map()6 );7 8 useEffect(() => {9 async function fetchData() {10 const now = Date.now();11 const cached = cacheRef.current.get(url);12 13 // Return cached data if available and not stale14 if (cached && (now - cached.timestamp) < staleTime) {15 setData(cached.data);16 setIsStale(false);17 return;18 }19 20 // If cached but stale, mark as stale and fetch in background21 if (cached) {22 setData(cached.data);23 setIsStale(true);24 } else {25 setData(null);26 setIsStale(false);27 }28 29 // Always fetch fresh data30 try {31 const response = await fetch(url);32 const freshData = await response.json();33 const entry = { data: freshData, timestamp: now };34 cacheRef.current.set(url, entry);35 setData(freshData);36 setIsStale(false);37 } catch (error) {38 if (cached) setIsStale(true);39 }40 }41 42 fetchData();43 }, [url, staleTime]);44 45 return { data, isStale };46}

Custom Hooks vs. Libraries

When to Build Custom Hooks

Building custom hooks makes sense when you have highly specialized requirements that existing libraries don't address well, when minimizing bundle size is paramount and you only need basic functionality, or when the team wants full control over behavior.

When to Use Libraries

Using libraries like SWR or TanStack Query makes sense for most production applications:

FeatureSWRTanStack Query
Bundle Size5.3KB16.2KB
DevToolsCommunityOfficial
API StyleArgumentsObject config
EcosystemVercel-focusedMulti-framework

The SWR library (stale-while-revalidate) is developed by Vercel and integrates seamlessly with Next.js applications. TanStack Query (formerly React Query) offers comprehensive features including DevTools for debugging and a larger plugin ecosystem.

For most teams, using an established library is recommended--the engineering cost of building and maintaining a production-grade data fetching solution almost always exceeds the cost of importing a library. When building AI-powered applications that require sophisticated data management, these libraries provide battle-tested foundations that accelerate development.

Performance Best Practices

Key optimizations for production-ready fetch hooks

Request Deduplication

Implement a request registry to prevent duplicate requests when multiple components request the same URL.

Proper Dependencies

Include only necessary dependencies in useEffect to avoid excessive refetching.

Memoization

Use useMemo for expensive data transformations to avoid recalculation on every render.

AbortController

Cancel pending requests on unmount to prevent memory leaks and state updates on unmounted components.

LRU Caching

Implement least-recently-used eviction to bound memory consumption.

Monitoring

Add logging and metrics to understand fetch patterns and diagnose issues.

Frequently Asked Questions

Need Help Building Performant React Applications?

Our team specializes in modern React development with Next.js, implementing best practices for data fetching, caching, and performance optimization.