๐ Data Fetching Basics
Most React applications need to fetch data from APIs. Whether you're building a weather app, a social media feed, or an e-commerce site, you'll need to request data from servers and display it to users. In this lesson, you'll learn how to fetch data using the Fetch API, handle loading and error states gracefully, type your API responses with TypeScript, and build production-ready data-fetching components. By the end, you'll be able to integrate any REST API into your React applications! ๐
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Understand the Fetch API and how it works
- Fetch data from REST APIs using useEffect
- Handle loading states while data fetches
- Handle and display error states properly
- Type API responses with TypeScript interfaces
- Cancel fetch requests to prevent memory leaks
- Use async/await syntax in effects
- Build reusable data-fetching components
- Implement common patterns (refetch, pagination)
- Debug network requests effectively
Estimated Time: 75-90 minutes
Project: Build a user profile viewer and a posts list with real API data
๐ In This Lesson
๐ Understanding the Fetch API
Before we integrate fetch into React, let's understand how the Fetch API works in JavaScript.
๐ Definition
Fetch API: A modern JavaScript interface for making HTTP requests to servers. It returns Promises and provides a cleaner alternative to XMLHttpRequest.
Basic Fetch Syntax
Simple GET Request
// Basic fetch - returns a Promise
fetch('https://api.example.com/data')
.then(response => response.json()) // Parse JSON
.then(data => console.log(data)) // Use data
.catch(error => console.error(error)); // Handle errors
// With async/await (cleaner)
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
How Fetch Works
๐ฎ Interactive: Watch a Fetch Request
Click the button to simulate a fetch request and watch the data flow:
GET /users/1
Waiting...
The Response Object
Understanding the Response
const response = await fetch('https://api.example.com/data');
// Response properties
console.log(response.ok); // true if status 200-299
console.log(response.status); // HTTP status code (200, 404, etc.)
console.log(response.statusText); // Status text ("OK", "Not Found")
console.log(response.headers); // Response headers
// Getting data from response
const json = await response.json(); // Parse as JSON
const text = await response.text(); // Get as text
const blob = await response.blob(); // Get as binary data
const formData = await response.formData(); // Get as form data
HTTP Methods
Different Request Types
// GET (default) - Retrieve data
fetch('https://api.example.com/users');
// POST - Create data
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
});
// PUT - Update data (replace entire resource)
fetch('https://api.example.com/users/1', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Alice Smith', email: 'alice@example.com' })
});
// PATCH - Update data (modify specific fields)
fetch('https://api.example.com/users/1', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Alice Smith' })
});
// DELETE - Remove data
fetch('https://api.example.com/users/1', {
method: 'DELETE'
});
Request Options
Configuring Fetch Requests
fetch('https://api.example.com/data', {
method: 'GET', // HTTP method
headers: { // Request headers
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify(data), // Request body (for POST/PUT/PATCH)
mode: 'cors', // CORS mode
credentials: 'include', // Send cookies
cache: 'no-cache', // Cache mode
redirect: 'follow', // Redirect behavior
signal: abortController.signal // For cancellation
});
Public APIs for Practice
๐ก Free APIs to Use
| API | URL | Description |
|---|---|---|
| JSONPlaceholder | jsonplaceholder.typicode.com | Fake REST API for testing |
| PokรฉAPI | pokeapi.co | Pokรฉmon data |
| OpenWeather | openweathermap.org | Weather data (API key required) |
| Dog API | dog.ceo/dog-api | Random dog images |
| Rick and Morty | rickandmortyapi.com | TV show data |
We'll use JSONPlaceholder for examples - it's perfect for learning!
โก Fetch with useEffect
Now let's integrate fetch into React components using useEffect. This is where data fetching happens in React!
Basic Pattern
Fetching Data on Mount
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
// Fetch user data when component mounts
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(data => setUser(data));
}, []); // Empty array = run once on mount
if (!user) {
return <div>Loading...</div>;
}
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
Why useEffect?
๐ก Why Not Fetch During Render?
// โ WRONG: Fetching during render
const BadComponent: React.FC = () => {
const [data, setData] = useState(null);
// This runs on EVERY render!
fetch('/api/data')
.then(res => res.json())
.then(setData); // Causes re-render
// Re-render causes fetch again... infinite loop! ๐ฅ
return <div>{data}</div>;
};
// โ
CORRECT: Fetching in useEffect
const GoodComponent: React.FC = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []); // Runs once on mount
return <div>{data}</div>;
};
Fetching Based on Props
Re-fetch When Data Changes
interface UserProfileProps {
userId: number;
}
const UserProfile: React.FC<UserProfileProps> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
// Fetch different user when userId changes
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(response => response.json())
.then(data => setUser(data));
}, [userId]); // Re-run when userId changes
if (!user) {
return <div>Loading...</div>;
}
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};
// Usage
<UserProfile userId={1} /> // Fetches user 1
<UserProfile userId={2} /> // Fetches user 2
Multiple Fetches
Fetching Multiple Resources
const Dashboard: React.FC = () => {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [todos, setTodos] = useState([]);
useEffect(() => {
// Fetch user
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(res => res.json())
.then(setUser);
// Fetch posts
fetch('https://jsonplaceholder.typicode.com/posts?userId=1')
.then(res => res.json())
.then(setPosts);
// Fetch todos
fetch('https://jsonplaceholder.typicode.com/todos?userId=1')
.then(res => res.json())
.then(setTodos);
}, []);
return (
<div>
<h1>Dashboard</h1>
{/* Display data */}
</div>
);
};
Sequential Fetches
Fetching Data That Depends on Other Data
const UserWithPosts: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// First effect: Fetch user
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
// Second effect: Fetch posts after we have user
useEffect(() => {
if (!user) return; // Wait for user to load
fetch(`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`)
.then(res => res.json())
.then(setPosts);
}, [user]); // Run when user changes
if (!user) return <div>Loading user...</div>;
if (posts.length === 0) return <div>Loading posts...</div>;
return (
<div>
<h2>{user.name}'s Posts</h2>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</article>
))}
</div>
);
};
โณ Loading States
Good UX means showing users what's happening. Loading states tell users "we're working on itโplease wait!"
Basic Loading State
Simple Loading Indicator
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true); // Start loading
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(data => {
setUser(data);
setIsLoading(false); // Done loading
});
}, []);
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h2>{user?.name}</h2>
<p>{user?.email}</p>
</div>
);
};
Loading State Flow
๐ฎ Interactive: Loading State Machine
Click buttons to simulate different fetch outcomes and see how states change:
Click "Start Fetch" to begin
Better Loading UI
Spinner Component
const LoadingSpinner: React.FC = () => (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(data => {
setUser(data);
setIsLoading(false);
});
}, []);
if (isLoading) {
return <LoadingSpinner />;
}
return (
<div>
<h2>{user?.name}</h2>
<p>{user?.email}</p>
</div>
);
};
Skeleton Screens
Show Content Structure While Loading
const UserSkeleton: React.FC = () => (
<div className="user-skeleton">
<div className="skeleton skeleton-avatar"></div>
<div className="skeleton skeleton-title"></div>
<div className="skeleton skeleton-text"></div>
<div className="skeleton skeleton-text"></div>
</div>
);
// CSS for skeleton
/*
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 4px;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
*/
Inline Loading State
Show Loading Without Replacing Content
const PostsList: React.FC = () => {
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const loadMore = () => {
setIsLoading(true);
fetch('https://jsonplaceholder.typicode.com/posts?_limit=10')
.then(res => res.json())
.then(newPosts => {
setPosts([...posts, ...newPosts]);
setIsLoading(false);
});
};
return (
<div>
<h2>Posts</h2>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</article>
))}
{/* Show button or loading spinner */}
{isLoading ? (
<div>Loading more...</div>
) : (
<button onClick={loadMore}>Load More</button>
)}
</div>
);
};
โ Loading State Best Practices
- Always show loading state: Never leave users wondering
- Be specific: "Loading posts..." is better than "Loading..."
- Use skeletons for complex UI: Shows structure, feels faster
- Show progress if possible: Progress bars when applicable
- Keep it fast: If loading takes >3 seconds, explain why
โ Error Handling
Networks fail. APIs go down. Users lose connection. Proper error handling is essential for production apps!
Basic Error Handling
Catching Fetch Errors
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(data => {
setUser(data);
setIsLoading(false);
})
.catch(err => {
setError(err.message);
setIsLoading(false);
});
}, []);
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<div>
<h2>{user?.name}</h2>
<p>{user?.email}</p>
</div>
);
};
HTTP Status Error Handling
Check Response Status
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(response => {
// Check if response is OK (status 200-299)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
setUser(data);
setIsLoading(false);
})
.catch(err => {
setError(err.message);
setIsLoading(false);
});
}, [userId]);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>{user?.name}</h2>
<p>{user?.email}</p>
</div>
);
};
Error Types
Different Error Scenarios
const fetchWithErrorHandling = async (url: string) => {
try {
const response = await fetch(url);
// Network error (offline, DNS failure, etc.)
if (!response.ok) {
// HTTP error status codes
switch (response.status) {
case 404:
throw new Error('Resource not found');
case 401:
throw new Error('Unauthorized - please login');
case 403:
throw new Error('Forbidden - no access');
case 500:
throw new Error('Server error - try again later');
default:
throw new Error(`Error: ${response.status}`);
}
}
const data = await response.json();
return data;
} catch (error) {
// Network failure (no internet, CORS, etc.)
if (error instanceof TypeError) {
throw new Error('Network error - check your connection');
}
// JSON parse error
if (error instanceof SyntaxError) {
throw new Error('Invalid response format');
}
// Re-throw other errors
throw error;
}
};
Better Error UI
User-Friendly Error Display
interface ErrorDisplayProps {
error: string;
onRetry?: () => void;
}
const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ error, onRetry }) => (
<div className="error-display">
<div className="error-icon">โ</div>
<h3>Oops! Something went wrong</h3>
<p>{error}</p>
{onRetry && (
<button onClick={onRetry}>Try Again</button>
)}
</div>
);
// Usage
const UserProfile: React.FC = () => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const fetchUser = () => {
setIsLoading(true);
setError(null);
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => {
if (!response.ok) throw new Error('Failed to load user');
return response.json();
})
.then(setUser)
.catch(err => setError(err.message))
.finally(() => setIsLoading(false));
};
useEffect(() => {
fetchUser();
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <ErrorDisplay error={error} onRetry={fetchUser} />;
return <div>{user?.name}</div>;
};
โ Error Handling Best Practices
- Always handle errors: Never leave users with broken UI
- Check response.ok: Fetch doesn't throw on HTTP errors
- Provide context: Tell users what went wrong
- Offer recovery: Retry buttons, alternative actions
- Log errors: Send to error tracking service in production
- Don't expose technical details: "Server error" not "500 Internal"
๐ Typing API Responses
TypeScript makes APIs safer and easier to work with. Let's learn how to properly type our API responses!
Defining Response Types
Create Interfaces for API Data
// types/user.ts
export interface User {
id: number;
name: string;
username: string;
email: string;
address: Address;
phone: string;
website: string;
company: Company;
}
export interface Address {
street: string;
suite: string;
city: string;
zipcode: string;
geo: Geo;
}
export interface Geo {
lat: string;
lng: string;
}
export interface Company {
name: string;
catchPhrase: string;
bs: string;
}
// types/post.ts
export interface Post {
userId: number;
id: number;
title: string;
body: string;
}
// types/comment.ts
export interface Comment {
postId: number;
id: number;
name: string;
email: string;
body: string;
}
Using Types with useState
Type State Correctly
import { useState, useEffect } from 'react';
import { User } from './types/user';
const UserProfile: React.FC = () => {
// Type as User | null (could be null while loading)
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then((data: User) => { // Type the data
setUser(data);
setIsLoading(false);
})
.catch((err: Error) => {
setError(err.message);
setIsLoading(false);
});
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
// TypeScript knows user is User here!
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>{user.address.city}</p> {/* TypeScript autocomplete! */}
</div>
);
};
Typing Fetch Functions
Generic Fetch Function
// utils/api.ts
export async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data as T;
}
// Usage
import { fetchData } from './utils/api';
import { User } from './types/user';
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchData<User>('https://jsonplaceholder.typicode.com/users/1')
.then(setUser)
.catch(console.error);
}, []);
// user is typed as User!
};
Validating API Responses
Runtime Type Checking
// Type guards
function isUser(data: any): data is User {
return (
typeof data === 'object' &&
data !== null &&
typeof data.id === 'number' &&
typeof data.name === 'string' &&
typeof data.email === 'string'
);
}
// Usage
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(data => {
if (isUser(data)) {
setUser(data);
} else {
throw new Error('Invalid user data');
}
})
.catch(err => setError(err.message));
}, []);
// ...
};
Using Zod for Validation
๐ก Better Validation with Zod
// Install: npm install zod
import { z } from 'zod';
// Define schema
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
username: z.string(),
phone: z.string(),
website: z.string()
});
// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// Validate API response
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(data => {
// Validate and parse
const validatedUser = UserSchema.parse(data);
setUser(validatedUser);
})
.catch(err => setError(err.message));
}, []);
// ...
};
โก Async/Await in Effects
Async/await makes asynchronous code much more readable. Let's learn how to use it properly with useEffect!
The Problem: Can't Make Effect Async
โ ๏ธ This Doesn't Work
// โ WRONG: Effect function can't be async
useEffect(async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
setData(data);
}, []);
// TypeScript Error:
// Effect callbacks are synchronous to prevent race conditions.
// Put the async function inside the effect.
Solution 1: Define Async Function Inside
โ Correct Pattern
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Define async function inside effect
const fetchUser = async () => {
try {
setIsLoading(true);
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUser(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setIsLoading(false);
}
};
// Call the async function
fetchUser();
}, []);
if (isLoading) 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>
);
};
Solution 2: Immediately Invoked Async Function
IIFE Pattern
useEffect(() => {
// Immediately invoked async function
(async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
}
})();
}, []);
Multiple Async Operations
Sequential Fetches
useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
// Fetch user first
const userResponse = await fetch('https://jsonplaceholder.typicode.com/users/1');
const userData = await userResponse.json();
setUser(userData);
// Then fetch their posts
const postsResponse = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userData.id}`);
const postsData = await postsResponse.json();
setPosts(postsData);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
Parallel Fetches with Promise.all
Fetch Multiple Resources Simultaneously
useEffect(() => {
const fetchAllData = async () => {
try {
setIsLoading(true);
// Fetch all at once
const [userResponse, postsResponse, todosResponse] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/users/1'),
fetch('https://jsonplaceholder.typicode.com/posts?userId=1'),
fetch('https://jsonplaceholder.typicode.com/todos?userId=1')
]);
// Parse all responses
const [userData, postsData, todosData] = await Promise.all([
userResponse.json(),
postsResponse.json(),
todosResponse.json()
]);
setUser(userData);
setPosts(postsData);
setTodos(todosData);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchAllData();
}, []);
// Promise.all is faster because requests happen in parallel!
// Sequential: 300ms + 300ms + 300ms = 900ms
// Parallel: max(300ms, 300ms, 300ms) = 300ms
Error Handling with Try-Catch
Comprehensive Error Handling
useEffect(() => {
const fetchUser = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
// Check response status
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
const data = await response.json();
// Validate data
if (!data.id || !data.name) {
throw new Error('Invalid user data received');
}
setUser(data);
} catch (err) {
// Handle different error types
if (err instanceof TypeError) {
setError('Network error - please check your connection');
} else if (err instanceof SyntaxError) {
setError('Invalid response format');
} else if (err instanceof Error) {
setError(err.message);
} else {
setError('An unknown error occurred');
}
} finally {
// Always runs, even if there's an error
setIsLoading(false);
}
};
fetchUser();
}, []);
โ Async/Await Best Practices
- Define async function inside effect: Don't make effect itself async
- Always use try-catch: Handle errors properly
- Use finally: For cleanup like setting loading to false
- Use Promise.all: For parallel requests
- Check response.ok: Before parsing JSON
- Type your data: Add type annotations to responses
๐ Canceling Requests
When components unmount or dependencies change, we need to cancel in-flight requests to prevent memory leaks and race conditions!
The Problem: Memory Leaks
โ ๏ธ Without Cancellation
// โ PROBLEM: Request completes after unmount
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(res => res.json())
.then(setUser); // What if component unmounted?
}, [userId]);
return <div>{user?.name}</div>;
};
// Scenario:
// 1. userId = 1, fetch starts
// 2. userId changes to 2, new fetch starts
// 3. First fetch completes (slow network)
// 4. setUser called with old data!
// Result: Wrong user displayed! ๐ฅ
Solution: AbortController
Properly Canceling Requests
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Create abort controller
const controller = new AbortController();
const fetchUser = async () => {
try {
setIsLoading(true);
// Pass signal to fetch
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`,
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data = await response.json();
setUser(data);
} catch (err) {
// Don't set error if request was aborted
if (err instanceof Error && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setIsLoading(false);
}
};
fetchUser();
// Cleanup: Cancel request on unmount or userId change
return () => {
controller.abort();
};
}, [userId]);
if (isLoading) 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>
);
};
How AbortController Works
๐ฎ Interactive: Request Cancellation
See how AbortController prevents race conditions when requests are cancelled:
Using with Async Functions
Pattern with Try-Catch
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(url, {
signal: controller.signal
});
const data = await response.json();
setData(data);
} catch (err) {
// Check if error is from abort
if (err instanceof Error) {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
return; // Don't set error state
}
setError(err.message);
}
}
};
fetchData();
return () => {
controller.abort();
};
}, [url]);
Race Condition Prevention
Search Component Example
const Search: React.FC = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
const controller = new AbortController();
const searchAPI = async () => {
try {
setIsLoading(true);
const response = await fetch(
`https://api.example.com/search?q=${query}`,
{ signal: controller.signal }
);
const data = await response.json();
setResults(data);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
console.error(err);
}
} finally {
setIsLoading(false);
}
};
searchAPI();
// Cancel when query changes
return () => {
controller.abort();
};
}, [query]);
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{isLoading && <div>Searching...</div>}
<ul>
{results.map(result => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</div>
);
};
Multiple Requests Cancellation
Cancel All Requests on Cleanup
useEffect(() => {
const controller = new AbortController();
const fetchAllData = async () => {
try {
// Use same signal for all requests
const [usersRes, postsRes] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/users', {
signal: controller.signal
}),
fetch('https://jsonplaceholder.typicode.com/posts', {
signal: controller.signal
})
]);
const [users, posts] = await Promise.all([
usersRes.json(),
postsRes.json()
]);
setUsers(users);
setPosts(posts);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err.message);
}
}
};
fetchAllData();
// Aborts ALL requests using this controller
return () => {
controller.abort();
};
}, []);
โ Request Cancellation Best Practices
- Always use AbortController: For all fetch requests in effects
- Check for AbortError: Don't treat it as a real error
- One controller per effect: Create new controller in each effect run
- Cancel in cleanup: Return cleanup function that calls abort()
- Don't update state after abort: Check for AbortError first
๐ฏ Complete Data Fetching Pattern
Let's put everything together into a production-ready pattern that handles all edge cases!
The Complete Pattern
Full-Featured Data Fetching Component
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
email: string;
username: string;
}
interface UserProfileProps {
userId: number;
}
const UserProfile: React.FC<UserProfileProps> = ({ userId }) => {
// State
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Create abort controller
const controller = new AbortController();
const fetchUser = async () => {
try {
// Reset states
setIsLoading(true);
setError(null);
// Fetch with cancellation support
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`,
{ signal: controller.signal }
);
// Check response status
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Parse JSON
const data: User = await response.json();
// Validate data
if (!data.id || !data.name) {
throw new Error('Invalid user data');
}
// Update state
setUser(data);
} catch (err) {
// Don't set error if aborted
if (err instanceof Error && err.name === 'AbortError') {
console.log('Fetch aborted');
return;
}
// Handle different error types
if (err instanceof TypeError) {
setError('Network error - check your connection');
} else if (err instanceof Error) {
setError(err.message);
} else {
setError('An unknown error occurred');
}
} finally {
// Always set loading to false
setIsLoading(false);
}
};
fetchUser();
// Cleanup: cancel request
return () => {
controller.abort();
};
}, [userId]); // Re-fetch when userId changes
// Loading state
if (isLoading) {
return (
<div className="loading">
<div className="spinner"></div>
<p>Loading user...</p>
</div>
);
}
// Error state
if (error) {
return (
<div className="error">
<h3>โ Error</h3>
<p>{error}</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
// No data state
if (!user) {
return (
<div className="empty">
<p>No user found</p>
</div>
);
}
// Success state
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>Username: {user.username}</p>
<p>Email: {user.email}</p>
</div>
);
};
export default UserProfile;
Reusable Fetch Hook
Custom Hook for Data Fetching
// hooks/useFetch.ts
import { useState, useEffect } from 'react';
interface UseFetchResult<T> {
data: T | null;
isLoading: boolean;
error: string | null;
refetch: () => void;
}
export function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [refetchTrigger, setRefetchTrigger] = useState(0);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetch(url, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setIsLoading(false);
}
};
fetchData();
return () => {
controller.abort();
};
}, [url, refetchTrigger]);
const refetch = () => {
setRefetchTrigger(prev => prev + 1);
};
return { data, isLoading, error, refetch };
}
// Usage
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const { data: user, isLoading, error, refetch } = useFetch<User>(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error} <button onClick={refetch}>Retry</button></div>;
if (!user) return <div>No user</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={refetch}>Refresh</button>
</div>
);
};
State Machine Pattern
Better State Management
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const [state, setState] = useState<FetchState<User>>({
status: 'idle'
});
useEffect(() => {
const controller = new AbortController();
const fetchUser = async () => {
setState({ status: 'loading' });
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`,
{ signal: controller.signal }
);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setState({ status: 'success', data });
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setState({ status: 'error', error: err.message });
}
}
};
fetchUser();
return () => controller.abort();
}, [userId]);
// Render based on status
switch (state.status) {
case 'idle':
case 'loading':
return <div>Loading...</div>;
case 'error':
return <div>Error: {state.error}</div>;
case 'success':
return (
<div>
<h2>{state.data.name}</h2>
<p>{state.data.email}</p>
</div>
);
}
};
๐จ Common Patterns
Let's explore practical patterns you'll use frequently in real applications!
Pattern 1: Manual Refetch
Refresh Button
const UserProfile: React.FC = () => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
useEffect(() => {
const fetchUser = async () => {
setIsLoading(true);
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
const data = await response.json();
setUser(data);
setIsLoading(false);
};
fetchUser();
}, [refreshKey]); // Re-run when refreshKey changes
const handleRefresh = () => {
setRefreshKey(prev => prev + 1);
};
return (
<div>
{isLoading ? (
<div>Loading...</div>
) : (
<>
<h2>{user?.name}</h2>
<button onClick={handleRefresh} disabled={isLoading}>
{isLoading ? 'Refreshing...' : 'Refresh'}
</button>
</>
)}
</div>
);
};
Pattern 2: Polling (Auto-Refresh)
Fetch Data on Interval
const LiveData: React.FC = () => {
const [data, setData] = useState(null);
const [isPolling, setIsPolling] = useState(true);
useEffect(() => {
if (!isPolling) return;
const fetchData = async () => {
const response = await fetch('https://api.example.com/live-data');
const json = await response.json();
setData(json);
};
// Fetch immediately
fetchData();
// Then fetch every 5 seconds
const intervalId = setInterval(fetchData, 5000);
return () => {
clearInterval(intervalId);
};
}, [isPolling]);
return (
<div>
<h2>Live Data</h2>
<button onClick={() => setIsPolling(!isPolling)}>
{isPolling ? 'Stop Polling' : 'Start Polling'}
</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
Pattern 3: Pagination
Load More Pattern
const PostsList: React.FC = () => {
const [posts, setPosts] = useState<Post[]>([]);
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
useEffect(() => {
const fetchPosts = async () => {
setIsLoading(true);
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=10`
);
const newPosts = await response.json();
if (newPosts.length === 0) {
setHasMore(false);
} else {
setPosts(prev => [...prev, ...newPosts]);
}
setIsLoading(false);
};
fetchPosts();
}, [page]);
const loadMore = () => {
setPage(prev => prev + 1);
};
return (
<div>
<h2>Posts</h2>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</article>
))}
{isLoading && <div>Loading more...</div>}
{hasMore && !isLoading && (
<button onClick={loadMore}>Load More</button>
)}
{!hasMore && <div>No more posts</div>}
</div>
);
};
Pattern 4: Dependent Fetches
Fetch Data Based on Other Data
const UserWithPosts: React.FC<{ userId: number }> = ({ userId }) => {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [isLoadingUser, setIsLoadingUser] = useState(true);
const [isLoadingPosts, setIsLoadingPosts] = useState(false);
// Fetch user first
useEffect(() => {
const fetchUser = async () => {
setIsLoadingUser(true);
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const data = await response.json();
setUser(data);
setIsLoadingUser(false);
};
fetchUser();
}, [userId]);
// Fetch posts after we have user
useEffect(() => {
if (!user) return;
const fetchPosts = async () => {
setIsLoadingPosts(true);
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`
);
const data = await response.json();
setPosts(data);
setIsLoadingPosts(false);
};
fetchPosts();
}, [user]);
if (isLoadingUser) return <div>Loading user...</div>;
if (isLoadingPosts) return <div>Loading posts...</div>;
return (
<div>
<h2>{user?.name}'s Posts</h2>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</article>
))}
</div>
);
};
Pattern 5: Caching Results
Simple Cache Implementation
// Simple cache outside component
const cache = new Map<string, any>();
const useCachedFetch = <T,>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check cache first
if (cache.has(url)) {
setData(cache.get(url));
setIsLoading(false);
return;
}
const fetchData = async () => {
const response = await fetch(url);
const json = await response.json();
// Save to cache
cache.set(url, json);
setData(json);
setIsLoading(false);
};
fetchData();
}, [url]);
return { data, isLoading };
};
๐๏ธ Hands-on Practice
Time to build real data-fetching components!
๐๏ธ Exercise 1: User Directory
Build a component that displays a list of users from the API.
Requirements:
- Fetch all users from:
https://jsonplaceholder.typicode.com/users - Display name, email, and city for each user
- Show loading state while fetching
- Handle and display errors
- Add a refresh button
Starter Code:
interface User {
id: number;
name: string;
email: string;
address: {
city: string;
};
}
const UserDirectory: React.FC = () => {
// Your code here!
return (
<div>
<h1>User Directory</h1>
{/* Display users */}
</div>
);
};
๐ก Hint
Use useState for users array, loading, and error. Use useEffect with empty dependency array to fetch on mount.
โ Solution
const UserDirectory: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchUsers = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchUsers();
}, []);
if (isLoading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>User Directory</h1>
<button onClick={fetchUsers}>Refresh</button>
<ul>
{users.map(user => (
<li key={user.id}>
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
<p>City: {user.address.city}</p>
</li>
))}
</ul>
</div>
);
};
๐๏ธ Exercise 2: Post Viewer
Create a component that shows a single post with its comments.
Requirements:
- Accept postId as prop
- Fetch post:
https://jsonplaceholder.typicode.com/posts/{postId} - Fetch comments:
https://jsonplaceholder.typicode.com/comments?postId={postId} - Use Promise.all to fetch both simultaneously
- Handle loading and error states
- Cancel requests when component unmounts
โ Solution
interface Post {
id: number;
title: string;
body: string;
}
interface Comment {
id: number;
name: string;
email: string;
body: string;
}
const PostViewer: React.FC<{ postId: number }> = ({ postId }) => {
const [post, setPost] = useState<Post | null>(null);
const [comments, setComments] = useState<Comment[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setIsLoading(true);
const [postRes, commentsRes] = await Promise.all([
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
signal: controller.signal
}),
fetch(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`, {
signal: controller.signal
})
]);
const [postData, commentsData] = await Promise.all([
postRes.json(),
commentsRes.json()
]);
setPost(postData);
setComments(commentsData);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setIsLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [postId]);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!post) return <div>Post not found</div>;
return (
<div>
<article>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
<section>
<h3>Comments ({comments.length})</h3>
{comments.map(comment => (
<div key={comment.id}>
<h4>{comment.name}</h4>
<p>{comment.body}</p>
<small>{comment.email}</small>
</div>
))}
</section>
</div>
);
};
๐๏ธ Challenge: Search & Filter
Build a searchable, filterable user list!
Requirements:
- Fetch all users
- Add search input to filter by name
- Add dropdown to filter by city
- Debounce search (wait 500ms after typing)
- Show "No results" when filters match nothing
- Display count of filtered users
โจ Best Practices
โ Do's
- Always handle loading states: Users need feedback
- Always handle errors: Networks fail, APIs go down
- Use AbortController: Cancel requests to prevent memory leaks
- Check response.ok: Fetch doesn't throw on HTTP errors
- Type your API responses: Create interfaces for data
- Use async/await: More readable than promise chains
- Validate API responses: Don't trust external data blindly
- Show specific error messages: Help users understand what went wrong
- Provide retry mechanisms: Let users recover from errors
- Use custom hooks: Extract reusable fetch logic
โ Don'ts
- Don't fetch during render: Always use useEffect
- Don't forget dependencies: Include all values used in effect
- Don't ignore AbortError: It's expected, not a real error
- Don't update state after unmount: Check if request was aborted
- Don't expose technical errors: Be user-friendly
- Don't fetch on every render: Use proper dependency arrays
- Don't forget empty states: Show when there's no data
- Don't block UI: Show loading indicators, not blank screens
- Don't trust response format: Validate data structure
Error Handling Strategy
Comprehensive Error Management
const fetchWithErrorHandling = async (url: string) => {
try {
const response = await fetch(url);
// HTTP errors
if (!response.ok) {
switch (response.status) {
case 400:
throw new Error('Bad request - check your input');
case 401:
throw new Error('Please log in to continue');
case 403:
throw new Error('You don\'t have permission');
case 404:
throw new Error('Resource not found');
case 500:
throw new Error('Server error - try again later');
case 503:
throw new Error('Service unavailable - try again later');
default:
throw new Error(`Error: ${response.status}`);
}
}
const data = await response.json();
return data;
} catch (err) {
// Network errors
if (err instanceof TypeError) {
throw new Error('Network error - check your connection');
}
// JSON parse errors
if (err instanceof SyntaxError) {
throw new Error('Invalid response format');
}
// Abort errors (don't treat as error)
if (err instanceof Error && err.name === 'AbortError') {
return null; // Or handle specially
}
// Re-throw other errors
throw err;
}
};
Loading State Best Practices
Better UX for Loading
// โ Bad: Just shows "Loading..."
if (isLoading) return <div>Loading...</div>;
// โ
Better: Skeleton screen
if (isLoading) {
return (
<div className="user-skeleton">
<div className="skeleton-avatar"></div>
<div className="skeleton-title"></div>
<div className="skeleton-text"></div>
</div>
);
}
// โ
Best: Specific message with spinner
if (isLoading) {
return (
<div className="loading-state">
<div className="spinner"></div>
<p>Loading user profile...</p>
</div>
);
}
API Organization
Centralize API Calls
// api/users.ts
const BASE_URL = 'https://jsonplaceholder.typicode.com';
export const usersAPI = {
getAll: async (): Promise<User[]> => {
const response = await fetch(`${BASE_URL}/users`);
if (!response.ok) throw new Error('Failed to fetch users');
return response.json();
},
getById: async (id: number): Promise<User> => {
const response = await fetch(`${BASE_URL}/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
},
create: async (user: Omit<User, 'id'>): Promise<User> => {
const response = await fetch(`${BASE_URL}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
if (!response.ok) throw new Error('Failed to create user');
return response.json();
},
update: async (id: number, user: Partial<User>): Promise<User> => {
const response = await fetch(`${BASE_URL}/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
if (!response.ok) throw new Error('Failed to update user');
return response.json();
},
delete: async (id: number): Promise<void> => {
const response = await fetch(`${BASE_URL}/users/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete user');
}
};
// Usage in component
const UserProfile: React.FC = () => {
const [user, setUser] = useState(null);
useEffect(() => {
usersAPI.getById(1)
.then(setUser)
.catch(console.error);
}, []);
// ...
};
Performance Considerations
๐ก Optimize Data Fetching
- Debounce search inputs: Don't fetch on every keystroke
- Use pagination: Don't load all data at once
- Implement caching: Avoid redundant requests
- Lazy load: Fetch data only when needed
- Use parallel requests: Promise.all for independent data
- Set timeouts: Don't let requests hang forever
- Optimize payload size: Request only needed fields
// Example: Request with timeout
const fetchWithTimeout = async (url: string, timeout = 5000) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return response;
} catch (err) {
clearTimeout(timeoutId);
throw err;
}
};
Security Considerations
Secure Data Fetching
- Never expose API keys in frontend code
- Use HTTPS: Always, never HTTP
- Sanitize user input: Before sending to API
- Validate responses: Don't trust API data blindly
- Handle sensitive data carefully: Don't log passwords, tokens
- Implement CORS properly: Understand cross-origin requests
- Use authentication tokens: Store securely, not in localStorage for sensitive apps
Testing Data Fetching
How to Test API Calls
// Mock fetch in tests
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ id: 1, name: 'Test User' })
})
) as jest.Mock;
// Test component
test('fetches and displays user', async () => {
render(<UserProfile userId={1} />);
// Check loading state
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('Test User')).toBeInTheDocument();
});
// Verify fetch was called
expect(fetch).toHaveBeenCalledWith(
'https://api.example.com/users/1'
);
});
๐ Summary
What You Learned
Congratulations! You've mastered data fetching in Reactโa critical skill for building real-world applications:
- โ Understanding the Fetch API and how it works
- โ Integrating fetch with useEffect for data fetching
- โ Managing loading states to provide user feedback
- โ Handling errors gracefully with proper error messages
- โ Typing API responses with TypeScript interfaces
- โ Using async/await for cleaner asynchronous code
- โ Canceling requests with AbortController
- โ Building reusable fetch hooks and patterns
- โ Implementing common patterns (pagination, polling, refetch)
- โ Following best practices for production apps
๐ฏ Key Takeaways
- Always use useEffect: Never fetch during render
- Handle three states: Loading, error, and success
- Cancel your requests: Use AbortController to prevent leaks
- Check response.ok: Fetch doesn't throw on HTTP errors
- Type everything: TypeScript makes APIs safer
- Think about UX: Good loading and error states matter
Data Fetching Checklist
โ Before You Ship
- โ Loading state implemented
- โ Error state handled
- โ Empty state shown when no data
- โ AbortController used for cancellation
- โ Response status checked (response.ok)
- โ API responses typed with TypeScript
- โ Error messages user-friendly
- โ Retry mechanism available
- โ No infinite loops or memory leaks
- โ Loading indicators visible and clear
Complete Example Reference
Full Pattern to Copy
import { useState, useEffect } from 'react';
interface DataType {
// Your data structure
}
const MyComponent: React.FC<{ id: number }> = ({ id }) => {
const [data, setData] = useState<DataType | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetch(`https://api.example.com/data/${id}`, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setIsLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [id]);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <div>No data</div>;
return <div>{/* Display data */}</div>;
};
Quick Reference
| Task | Code |
|---|---|
| Basic Fetch | fetch(url).then(r => r.json()) |
| With Async/Await | const res = await fetch(url); const data = await res.json(); |
| Check Status | if (!response.ok) throw new Error(); |
| Cancel Request | controller.abort() |
| Handle Abort | if (err.name !== 'AbortError') ... |
| POST Request | fetch(url, {method: 'POST', body: JSON.stringify(data)}) |
๐ What's Next?
In the next lesson, we'll learn about Custom Hooks:
- Creating reusable hook logic
- Building custom hooks for data fetching
- Hook composition and patterns
- Sharing logic between components
- Advanced custom hook techniques
You'll take your data fetching code and make it truly reusable! ๐ช
Additional Resources
๐ Further Reading
๐ ๏ธ Useful Libraries
- Axios: Popular alternative to fetch with better defaults
- React Query: Powerful data fetching and caching library
- SWR: React Hooks for data fetching by Vercel
- Zod: TypeScript-first schema validation
Congratulations! ๐
๐ You're Now a Data Fetching Pro!
You've learned how to fetch data from APIs, handle all edge cases, and build production-ready components. This is a fundamental skill that you'll use in every React application you build.
Key Achievement: You can now integrate any REST API into your React applications with confidence!
Keep practicing with different APIs, and you'll soon be building complex, data-driven applications! ๐