How to Professionally Communicate with a Server
Loading data via fetch is 10% of the work. The other 90% is handling all possible situations so the user doesn't stare at a blank screen.
1. Three States of Any Request
Never just do const [data, setData] = useState(). You need at least three variables or one complex status object:
- Loading: " I sent a messenger, waiting for response " . User should see a spinner or skeleton.
- Success: " Messenger returned with gold " . We show the data.
- Error: " Messenger was eaten by wolves " . We show a " Retry " button and a clear error message.
2. Where to Store Data?
We use useEffect to start loading.
- Why not in the function body? Because the function body runs on every render. If you write
fetchthere, you'll spam the server with requests thousands of times per minute. - In an effect with empty dependency array
[], the request will go exactly once.
3. Error Handling Is Not Optional
A bad developer hopes for perfect internet. A good one knows the server can go down and Wi-Fi can disconnect.
useEffect(() => {
setIsLoading(true);
setError(null);
fetch('/api/user')
.then(res => {
if (!res.ok) throw new Error('Server error');
return res.json();
})
.then(data => setData(data))
.catch(err => setError(err.message))
.finally(() => setIsLoading(false));
}, []);
4. Why Is This Hard?
As you saw in the previous lesson, manual data loading is full of traps:
- Must remember Race Conditions.
- Need to cache data (so you don't reload it on every page navigation).
- Need to be able to " stale " data (update it if it's outdated).
In the future we'll study libraries like TanStack Query that do all this automatically. But now it's important to understand the " manual " path to appreciate the magic of tools.