React.js Fundamentals

Data Fetching Patterns

18 min Lesson 24 of 40

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:

  1. Create search input with debounced queries (300ms)
  2. Use React Query to fetch search results
  3. Show loading state while searching
  4. Display "No results" when search returns empty
  5. Implement search result caching

Practice Exercise 2: Infinite Scroll Feed

Create an infinite scrolling posts feed:

  1. Use React Query's useInfiniteQuery hook
  2. Load 10 posts per page
  3. Implement scroll detection with IntersectionObserver
  4. Show loading spinner at bottom while fetching
  5. Add "Load More" button as fallback

Practice Exercise 3: Optimistic Todo Updates

Build a todo app with instant UI updates:

  1. Use SWR or React Query for todo list
  2. Implement optimistic updates for add/toggle/delete
  3. Show rollback message if server request fails
  4. Add retry mechanism for failed requests
  5. 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.