DEV Community

Abhay Singh Kathayat
Abhay Singh Kathayat

Posted on

Handling Async Operations in React with useEffect, Promises, and Custom Hooks

Handling Async Calls in React

Handling asynchronous operations is a common requirement in React applications, especially when working with APIs, databases, or external services. Since JavaScript operations like fetching data from an API or performing computations are often asynchronous, React provides tools and techniques to handle them gracefully.

In this guide, we'll explore different ways of handling asynchronous calls in React using async/await, Promises, and other React-specific tools.


1. Using useEffect for Async Calls

React’s useEffect hook is perfect for performing side-effects like fetching data when a component mounts. The hook itself cannot directly return a promise, so we use an async function inside the effect.

Using async/await inside useEffect

Here’s how to handle an asynchronous call to fetch data using the useEffect hook.

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

const API_URL = 'https://jsonplaceholder.typicode.com/posts'; // Example API

const AsyncFetchExample = () => {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Using async/await inside useEffect
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(API_URL);
        if (!response.ok) {
          throw new Error('Failed to fetch data');
        }
        const data = await response.json();
        setPosts(data);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData(); // Call the async function
  }, []); // Empty dependency array means this effect runs once when the component mounts

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

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default AsyncFetchExample;
Enter fullscreen mode Exit fullscreen mode
  • async/await: Handles the promise-based fetching process.
  • Error Handling: Catches errors and displays appropriate messages.
  • Loading State: Manages loading and displays a loading indicator until data is fetched.

Why use async/await?

  • Simplifies code: Avoids the need for .then() and .catch() chains.
  • Cleaner and more readable: Promises can be handled in a more linear way.

2. Using Promises for Async Operations

Another common approach is using Promises directly with then() and catch(). This method is less elegant than async/await but still widely used in JavaScript for handling asynchronous operations.

Using Promises in useEffect

Here’s an example using Promise and then() for async calls:

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

const API_URL = 'https://jsonplaceholder.typicode.com/posts';

const AsyncFetchWithPromise = () => {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(API_URL)
      .then((response) => {
        if (!response.ok) {
          throw new Error('Failed to fetch data');
        }
        return response.json();
      })
      .then((data) => {
        setPosts(data);
        setLoading(false);
      })
      .catch((error) => {
        setError(error.message);
        setLoading(false);
      });
  }, []);

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

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default AsyncFetchWithPromise;
Enter fullscreen mode Exit fullscreen mode
  • then(): Handles the successful resolution of the Promise.
  • catch(): Catches any errors that occur during the API call.
  • Error Handling: Displaying an error message if the request fails.

3. Using useReducer for Complex Async Logic

When you have more complex state transitions or need to handle multiple actions during an async process (like loading, success, error), useReducer is a great tool to manage state changes.

Using useReducer with Async Operations

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

const API_URL = 'https://jsonplaceholder.typicode.com/posts';

// Reducer function to manage loading, success, and error states
const reducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, posts: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
};

const AsyncFetchWithReducer = () => {
  const initialState = { posts: [], loading: true, error: null };
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_START' });
      try {
        const response = await fetch(API_URL);
        if (!response.ok) throw new Error('Failed to fetch data');
        const data = await response.json();
        dispatch({ type: 'FETCH_SUCCESS', payload: data });
      } catch (error) {
        dispatch({ type: 'FETCH_ERROR', payload: error.message });
      }
    };
    fetchData();
  }, []);

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

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {state.posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default AsyncFetchWithReducer;
Enter fullscreen mode Exit fullscreen mode
  • useReducer: A more robust state management tool for complex asynchronous logic.
  • Multiple actions: Handles different states like loading, success, and error separately.

4. Using Custom Hooks for Reusable Async Logic

In some cases, you might want to reuse asynchronous logic across multiple components. Creating a custom hook is an excellent way to encapsulate the logic and make your code more reusable.

Creating a Custom Hook for Fetching Data

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

// Custom hook to fetch data
const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Failed to fetch data');
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
};

const AsyncFetchWithCustomHook = () => {
  const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');

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

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {data.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default AsyncFetchWithCustomHook;
Enter fullscreen mode Exit fullscreen mode
  • Custom Hook (useFetch): Encapsulates the logic for fetching data, error handling, and loading state.
  • Reusability: The hook can be reused across any component that needs to fetch data.

5. Conclusion

Handling async operations in React is essential for building modern web applications. By using hooks like useEffect, useReducer, and custom hooks, you can easily manage asynchronous behavior, handle errors, and ensure smooth user experiences. Whether you're fetching data, handling errors, or performing complex async logic, React provides you with powerful tools to manage these tasks efficiently.


Top comments (0)