React Mastery / React Hooks

Managing Data Fetching with useState in React

When fetching data from an API inside a React component, we typically have to manage three main things in state:

  1. Loading → Are we still waiting for data?

  2. Data → What we got from the API.

  3. Error → Any issue that happened during the fetch.


Step-by-Step Approach

1. Set Up State Variables

We’ll use three useState hooks:

const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
const [error, setError] = useState('');
  • loading: starts as true until the API responds.

  • data: stores the fetched result; starts as null (or an empty object).

  • error: stores any error message; starts empty.


2. Fetch Data on Component Mount

Use useEffect with an empty dependency array ([]) so the fetch runs only once after the component renders.

useEffect(() => {
  fetch('<https://jsonplaceholder.typicode.com/posts/1>')
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not okay');
      }
      return response.json();
    })
    .then(result => {
      setLoading(false);
      setData(result);
      setError('');
    })
    .catch(err => {
      setLoading(false);
      setData(null);
      setError('Something went wrong: ' + err.message);
    });
}, []);
  • On success:

    • loading becomes false

    • data is updated with API result

    • error is cleared

  • On failure:

    • loading becomes false

    • data is set to null

    • error is updated with a message


3. Rendering Based on State

We show different UI elements depending on our state’s values:

if (loading) return <p>Loading...</p>;
if (error) return <p>{error}</p>;
return <h1>{data.title}</h1>;

Why This Works Well

  • Clear separation of concerns: Each piece of state controls one aspect of the UI (loading spinner, data, error).

  • Predictable data flow: The UI always reflects the exact fetch state.

  • Graceful error handling: If something fails, the user gets immediate feedback.

  • One-time fetch: The empty dependency array in useEffect ensures the API is called only once.


✅ When to Consider useReducer Instead

If you have more complex fetch logic, or multiple related states (loading/data/error) that you want to manage as a single object, useReducer can simplify your state updates and make them more predictable, especially for larger applications.


Example: Data Fetching with useState (Loading, Data, Error States)


How This Works

  1. Initial State

    • loading starts as true

    • data is null initially

    • error is an empty string

  2. Fetching Data

    • useEffect triggers the fetch once when the component mounts.

    • On success, we stop loading and set data.

    • On failure, we stop loading and set an error message.

  3. UI Feedback

    • If still loading → shows "Loading...".

    • If error → displays error in red.

    • If success → renders the requested post (title + body).