The Pattern
When fetching data in useEffect, you must handle three things:
- Loading State: Show a spinner while waiting.
- Error State: Show a message if it fails.
- Cleanup: Cancel the request if the component unmounts (Race Condition).
The Code
import { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
}
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// 1. Reset state on dependency change
setLoading(true);
setError(null);
setUser(null);
// 2. Create cancel flag
let isMounted = true;
const controller = new AbortController();
const fetchData = async () => {
try {
const res = await fetch(`/api/users/${userId}`, {
signal: controller.signal
});
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
// 3. Update state only if mounted
if (isMounted) {
setUser(data);
setLoading(false);
}
} catch (err) {
if (isMounted && (err as Error).name !== 'AbortError') {
setError((err as Error).message);
setLoading(false);
}
}
};
fetchData();
// 4. Cleanup function
return () => {
isMounted = false;
controller.abort();
};
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <h1>{user?.name}</h1>;
}
Sponsored Content
Google AdSense Placeholder
CONTENT SLOT