useEffect and Async Functions

React's useEffect hook is like telling your component, "After you’ve drawn yourself on the screen, run this side quest." But what happens when that side quest involves something asynchronous, like grabbing data from a server? You might reach for async/await, but there's a catch: you can’t make the function you pass to useEffect itself async. Why? Because useEffect expects a cleanup function or nothing — and an async function returns a Promise instead.

How to work around it

The workaround is simple: define an async function inside the useEffect callback, and then call it right away.

{`useEffect(() => {
  const fetchData = async () => {
    const response = await fetch('https://api.example.com/data');
    const json = await response.json();
    console.log(json);
  };

  fetchData();
}, []);`}

This pattern keeps useEffect synchronous, while still allowing asynchronous logic within it.

Real World Metaphor

Imagine your React component is a restaurant. The useEffect is like what happens after the restaurant opens (component mounts). You can’t say "open the restaurant asynchronously" — you need to open it, then handle background tasks, like calling your supplier (an async fetch). You don't want the door opening (UI mounting) to wait for the call.

File Setup for Practical Example

Here's a mini project to practice this idea. Create the following files:

You'll need create-react-app or a similar setup. Run:

npx create-react-app useeffect_async_demo

Step-by-Step Guide

In App.js:

{`import React, { useEffect, useState } from 'react';

function App() {
  const [quote, setQuote] = useState('');

  useEffect(() => {
    const fetchQuote = async () => {
      try {
        const response = await fetch('https://api.quotable.io/random');
        const data = await response.json();
        setQuote(data.content);
      } catch (error) {
        console.error('Error fetching quote:', error);
      }
    };

    fetchQuote();
  }, []);

  return (
    <div className="App">
      <h1>Random Quote</h1>
      <p>{quote}</p>
    </div>
  );
}

export default App;`}

Detailed Code Explanation

Practical Usage

Why is this important? In real-world applications, you often need to:

Common Mistakes

Bonus Example: AbortController

Let’s take it a step further. What if the component unmounts before the async task completes? Use an AbortController to clean up:

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

  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data', {
        signal: controller.signal
      });
      const data = await response.json();
      console.log(data);
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Fetch aborted');
      }
    }
  };

  fetchData();

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

Further Topics to Explore

Exercise

Create a simple app that:

  1. Fetches a joke from https://icanhazdadjoke.com/
  2. Displays the joke in the UI
  3. Includes a button to fetch a new joke

Use the same pattern shown above: async function inside useEffect.

Wrap Up

Just like you wouldn't try to make a pizza before preheating the oven, you shouldn’t mark a useEffect function as async directly. Let it warm up first (mount), then do your async tasks inside. With practice, handling asynchronous effects becomes second nature.