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:

  1. Loading: " I sent a messenger, waiting for response " . User should see a spinner or skeleton.
  2. Success: " Messenger returned with gold " . We show the data.
  3. 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 fetch there, 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.