Prior CoderTech Studio
By priorcoder
Student

Handling React Loading and Errors with Best Practices

Handling Loading & Errors in React

Modern web applications rely heavily on asynchronous operations—especially when fetching data from APIs. While this allows your app to stay responsive, it also introduces two important states you must handle properly:

  • Loading state (when data is being fetched)

  • Error state (when something goes wrong)

If you ignore these, your UI will feel broken, confusing, or unreliable.


Why Handling Loading and Errors Matters

When your React app fetches data:

  • The request takes time → user needs feedback (loading state)

  • The request may fail → user needs clarity (error state)

Without handling these:

  • Users may see blank screens

  • Debugging becomes harder

  • UX becomes poor

The correct approach is to use the useEffect Hook.


Basic Example: Fetching Data with useEffect

Here’s a simple and correct pattern:


import { useState, useEffect } from 'react';

function App() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch('https://example.com/data');
      const jsonData = await response.json();
      setData(jsonData);
    }

    fetchData();
  }, []);

  if (!data) {
    return <p>Loading...</p>;
  }

  return <div>Data: {JSON.stringify(data)}</div>;
}

What’s happening here?

  • useEffect runs once when the component mounts ([])

  • Data is fetched asynchronously

  • Once received, state updates → component re-renders

Adding Proper Loading and Error Handling

The above example works, but it’s incomplete. Let’s improve it by handling all states.

Improved Version

import { useState, useEffect } from 'react';

function App() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch('https://example.com/data');

        if (!response.ok) {
          throw new Error('Failed to fetch data');
        }

        const jsonData = await response.json();
        setData(jsonData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return <div>Data: {JSON.stringify(data)}</div>;
}

Understanding the States Clearly

1. Loading State

const [loading, setLoading] = useState(true);
  • Starts as true

  • Turns false after API completes

2. Error State

const [error, setError] = useState(null);
  • Stores error message

  • Helps display meaningful UI

3. Data State

const [data, setData] = useState(null);
  • Holds API response

  • Drives UI rendering


Avoiding Common Pitfalls

1. Infinite Loops

useEffect(() => {
  fetchData();
});

This runs on every render → infinite loop


Fix:


useEffect(() => {
  fetchData();
}, []);

2. Updating State After Unmount (Memory Leak)

If a component unmounts before the API finishes, calling setState causes warnings.

useEffect(() => {
  const controller = new AbortController();

  async function fetchData() {
    try {
      const response = await fetch('https://example.com/data', {
        signal: controller.signal,
      });
      const data = await response.json();
      setData(data);
    } catch (error) {
      if (error.name !== 'AbortError') {
        setError(error.message);
      }
    }
  }

  fetchData();

  return () => {
    controller.abort();
  };
}, []);

3. Multiple Requests Overwriting Each Other

If dependencies change quickly, older responses may override newer ones.


Fix:


  • Cancel previous requests

  • Or track request IDs

Better UI Patterns for Loading

Instead of plain text:


<p>Loading...</p>

Better

  • Skeleton loaders

  • Spinners

  • Placeholder UI

Example:


return loading ? <Spinner /> : <DataView data={data} />;

Real-World Best Practice: Custom Hook

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    async function fetchData() {
      try {
        const res = await fetch(url);
        const json = await res.json();

        if (isMounted) {
          setData(json);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

Usage:


function App() {
  const { data, loading, error } = useFetch('/api/data');

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return <div>{JSON.stringify(data)}</div>;
}


Answers & discussion

Sign in to comment.

No comments yet.