Data Fetching Patterns
Modern React applications use various data fetching patterns and libraries. From the native fetch API to specialized tools like React Query and SWR, choosing the right approach depends on your application's needs for caching, revalidation, and real-time updates.
Native Fetch API
The most basic approach using JavaScript's built-in fetch function:
// Basic fetch with hooks
import { useState, useEffect } from 'react';
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(
`https://api.example.com/users/${userId}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!cancelled) {
setUser(data);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchUser();
// Cleanup function to prevent state updates after unmount
return () => {
cancelled = true;
};
}, [userId]);
return { user, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { user, loading, error } = useUser(userId);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Fetch API Limitations:
- No automatic caching or revalidation
- Manual cleanup required to prevent memory leaks
- Must handle loading and error states manually
- No built-in request deduplication
- Boilerplate code repeats across components
Axios - Enhanced HTTP Client
Axios provides a more robust API with interceptors and better error handling:
// Install: npm install axios
import axios from 'axios';
import { useState, useEffect } from 'react';
// Create axios instance with defaults
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor - add auth token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - handle errors globally
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Redirect to login
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// Custom hook with axios
function useAxiosFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const source = axios.CancelToken.source();
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await api.get(url, {
...options,
cancelToken: source.token
});
setData(response.data);
} catch (err) {
if (!axios.isCancel(err)) {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => {
source.cancel('Component unmounted');
};
}, [url]);
return { data, loading, error };
}
// Usage
function Products() {
const { data, loading, error } = useAxiosFetch('/products');
if (loading) return <div>Loading products...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{data?.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
Axios Benefits: Automatic JSON transformation, request/response interceptors, request cancellation, timeout support, CSRF protection, and better error handling. It's more feature-rich than fetch but adds a dependency to your project.
SWR - Stale-While-Revalidate
SWR is a React Hooks library for data fetching with caching and automatic revalidation:
// Install: npm install swr
import useSWR from 'swr';
// Fetcher function - can be customized
const fetcher = (url) => fetch(url).then(res => res.json());
// Basic usage
function Profile() {
const { data, error, isLoading } = useSWR(
'/api/user',
fetcher
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Failed to load</div>;
return <div>Hello {data.name}!</div>;
}
// With options
function UserList() {
const { data, error, isLoading, mutate } = useSWR(
'/api/users',
fetcher,
{
refreshInterval: 3000, // Refetch every 3 seconds
revalidateOnFocus: true, // Revalidate on window focus
revalidateOnReconnect: true, // Revalidate on reconnect
dedupingInterval: 2000, // Dedupe requests within 2 seconds
onSuccess: (data) => {
console.log('Data loaded:', data);
}
}
);
const handleAddUser = async (newUser) => {
// Optimistic update
mutate([...data, newUser], false);
// Make API call
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser)
});
// Revalidate
mutate();
};
return (
<div>
{data?.map(user => (
<div key={user.id}>{user.name}</div>
))}
<button onClick={() => handleAddUser({ name: 'New User' })}>
Add User
</button>
</div>
);
}
// Dependent fetching
function UserPosts({ userId }) {
const { data: user } = useSWR(
userId ? `/api/users/${userId}` : null,
fetcher
);
const { data: posts } = useSWR(
user ? `/api/posts?userId=${user.id}` : null,
fetcher
);
return (
<div>
{posts?.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
SWR Caching: SWR caches data in memory and shares it across all components using the same key. This means multiple components requesting the same data will share one request. Be aware of cache invalidation when data updates.
React Query (TanStack Query)
The most powerful data fetching library with advanced caching and state management:
// Install: npm install @tanstack/react-query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Setup QueryClient in App
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 1,
refetchOnWindowFocus: false
}
}
});
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app */}
</QueryClientProvider>
);
}
// Basic query
function Posts() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('/api/posts');
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
}
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// Mutations
function CreatePost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newPost) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost)
});
return response.json();
},
onMutate: async (newPost) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts'] });
// Snapshot previous value
const previousPosts = queryClient.getQueryData(['posts']);
// Optimistically update
queryClient.setQueryData(['posts'], (old) => [...old, newPost]);
return { previousPosts };
},
onError: (err, newPost, context) => {
// Rollback on error
queryClient.setQueryData(['posts'], context.previousPosts);
},
onSettled: () => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: ['posts'] });
}
});
const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate({
title: e.target.title.value,
content: e.target.content.value
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit" disabled={mutation.isLoading}>
{mutation.isLoading ? 'Creating...' : 'Create Post'}
</button>
{mutation.isError && (
<div>Error: {mutation.error.message}</div>
)}
</form>
);
}
// Pagination
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, isLoading, isPreviousData } = useQuery({
queryKey: ['posts', page],
queryFn: async () => {
const response = await fetch(`/api/posts?page=${page}`);
return response.json();
},
keepPreviousData: true // Keep showing old data while fetching new
});
return (
<div>
{data?.posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
<button
onClick={() => setPage(old => Math.max(old - 1, 1))}
disabled={page === 1}
>
Previous
</button>
<button
onClick={() => setPage(old => old + 1)}
disabled={isPreviousData || !data?.hasMore}
>
Next
</button>
</div>
);
}
Choosing the Right Tool
When to Use Each:
- Fetch API: Simple one-off requests, small apps, learning
- Axios: Need interceptors, better error handling, request cancellation
- SWR: Real-time apps, need auto-revalidation, simpler API
- React Query: Complex apps, need advanced caching, mutations, pagination, infinite scroll
Advanced Pattern: Prefetching
Improve UX by prefetching data before users need it:
// React Query prefetching
import { useQueryClient } from '@tanstack/react-query';
function PostList() {
const queryClient = useQueryClient();
const handleMouseEnter = (postId) => {
// Prefetch post details on hover
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetch(`/api/posts/${postId}`).then(r => r.json())
});
};
return (
<div>
{posts.map(post => (
<Link
key={post.id}
to={`/posts/${post.id}`}
onMouseEnter={() => handleMouseEnter(post.id)}
>
{post.title}
</Link>
))}
</div>
);
}
Practice Exercise 1: Product Search with Debouncing
Build a searchable product list:
- Create search input with debounced queries (300ms)
- Use React Query to fetch search results
- Show loading state while searching
- Display "No results" when search returns empty
- Implement search result caching
Practice Exercise 2: Infinite Scroll Feed
Create an infinite scrolling posts feed:
- Use React Query's useInfiniteQuery hook
- Load 10 posts per page
- Implement scroll detection with IntersectionObserver
- Show loading spinner at bottom while fetching
- Add "Load More" button as fallback
Practice Exercise 3: Optimistic Todo Updates
Build a todo app with instant UI updates:
- Use SWR or React Query for todo list
- Implement optimistic updates for add/toggle/delete
- Show rollback message if server request fails
- Add retry mechanism for failed requests
- Display sync status indicator per todo
Summary
Choose your data fetching approach based on app complexity. Fetch API works for simple cases, Axios adds features, while SWR and React Query provide powerful caching and state management. Modern apps benefit from React Query's advanced features like optimistic updates, prefetching, and automatic cache invalidation.