JavaScript Promises in the useEffect hook

Efficiently Harness JavaScript Promises in ReactJS useEffect for Advanced Web Development

November 23, 2023 Dykraf

Discover how to boost your ReactJS applications by effectively using JavaScript Promises in the useEffect hook.

Web Story VersionWeb Story

In JavaScript, a Promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. It is a way to handle asynchronous code more easily and avoid callback hell.

Promises in JavaScript are objects representing the eventual completion or failure of an asynchronous operation and its resulting value. They provide a cleaner and more structured way to handle asynchronous code compared to traditional callbacks. Promises have three states: pending, fulfilled, and rejected.

The Promise((resolve, reject)) function

A Promise has three states:

  1. Pending: The initial state; the promise is neither fulfilled nor rejected.
  2. Fulfilled: The operation completed successfully, and the promise has a resulting value.
  3. Rejected: The operation failed, and the promise has a reason for the failure.

Promises provide a clean and organized way to work with asynchronous code, making it easier to reason about and maintain. They are commonly used in various JavaScript environments, including browser-based applications and server-side Node.js applications. The Promise object has two important methods: then() for handling the success case and catch() for handling failures. Additionally, the finally() method can be used to execute code, whether the promise is fulfilled or rejected.

Here's a simple example of using Promises within the useEffect hook in a React component to fetch data:

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

const MyComponent = () => {
  const [data, setData] = useState(null)

  useEffect(() => {
    // Define a function that returns a Promise to fetch data
    const fetchData = () => {
      return new Promise((resolve, reject) => {
        // Simulate a fetch request
        fetch('https://jsonplaceholder.typicode.com/todos/1')
          .then((response) => response.json())
          .then((data) => resolve(data))
          .catch((error) => reject(error))
      })
    }

    // Use the Promise to fetch data
    fetchData()
      .then((result) => {
        setData(result)
      })
      .catch((error) => {
        console.error('Error fetching data:', error)
      })
  }, []) // Empty dependency array means this effect runs once after the initial render

  // Render the component with the fetched data
  return (
    <div>
      <h1>Fetched Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}

export default MyComponent

In this example:

  1. The fetchData function returns a Promise that wraps the fetch API call to retrieve data from a placeholder API (https://jsonplaceholder.typicode.com/todos/1).

  2. The useEffect hook uses this Promise to fetch data when the component mounts ([] as the dependency array means it runs once after the initial render).

  3. The then method is used to handle the successful resolution of the Promise, updating the component's state with the fetched data.

  4. The catch method is used to handle any errors that may occur during the asynchronous operation.

This is a basic example, but it demonstrates the use of Promises to handle asynchronous operations within the useEffect hook in a React component. You can combine this with the useState hook to add and manage the result in states.

Demos: https://codepen.io/dyarfi/pen/mdQeWMW


Here's an example using useState and Promises within the useEffect hook to fetch data and update the component's state with a loading state:

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

const MyComponent = () => {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    // Define a function that returns a Promise to fetch data
    const fetchData = () => {
      return new Promise((resolve, reject) => {
        // Simulate a fetch request
        fetch('https://jsonplaceholder.typicode.com/todos/1')
          .then((response) => response.json())
          .then((data) => resolve(data))
          .catch((error) => reject(error))
      })
    }

    // Use the Promise to fetch data
    fetchData()
      .then((result) => {
        setData(result)
        setLoading(false)
      })
      .catch((error) => {
        setError(error)
        setLoading(false)
      })
  }, []) // Empty dependency array means this effect runs once after the initial render

  // Render the component based on the fetched data, loading state, and errors
  return (
    <div>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
      {data && (
        <div>
          <h1>Fetched Data:</h1>
          <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
      )}
    </div>
  )
}

export default MyComponent

In this example:

  1. I added two additional state variables, loading and error, to track the loading state and any errors during the data fetching process.

  2. The fetchData function returns a Promise, and within the useEffect hook, the component sets the loading state to true before fetching data.

  3. The then method updates the component's state with the fetched data and sets the loading state to false.

  4. The catch method handles any errors that occur during the fetch operation, updates the error state, and sets the loading state to false.

  5. The component's rendering logic takes into account the loading state and errors, displaying appropriate messages or the fetched data accordingly.

Demos: https://codepen.io/dyarfi/pen/jOdQNEB


The Promise.all() or Promise.race() to manage multiple asynchronous operations

Promises in JavaScript are well-suited for handling multiple asynchronous requests. You can create an array of Promises and use functions like Promise.all() or Promise.race() to manage multiple asynchronous operations.

  1. Promise.all(): This method takes an array of Promises and returns a new Promise that is fulfilled with an array of results when all of the input promises are fulfilled. If any of the input promises are rejected, the entire Promise.all() is rejected.

    const promise1 = fetchData1()
    const promise2 = fetchData2()
    
    Promise.all([promise1, promise2])
      .then(([result1, result2]) => {
        // Handle results
      })
      .catch((error) => {
        // Handle error
      })
    
  2. Promise.race(): This method takes an array of Promises and returns a new Promise that is fulfilled or rejected as soon as one of the input promises is fulfilled or rejected. It is useful when you only need the result of the first resolved promise.

    const promise1 = fetchData1()
    const promise2 = fetchData2()
    
    Promise.race([promise1, promise2])
      .then((result) => {
        // Handle the first resolved promise
      })
      .catch((error) => {
        // Handle error
      })
    

These methods allow you to efficiently manage concurrency and coordinate the handling of multiple asynchronous requests in your JavaScript code.

The Promise.all() function

Let's say you have a React component that needs to fetch data from two different endpoints using fetch API within the useEffect hook. You can use Promise.all() to wait for both requests to complete before updating the component's state. Here's an example:

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

const MyComponent = () => {
  const [data1, setData1] = useState(null)
  const [data2, setData2] = useState(null)

  useEffect(() => {
    const fetchData1 = () =>
      fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) =>
        response.json()
      )
    const fetchData2 = () =>
      fetch('https://jsonplaceholder.typicode.com/todos/2').then((response) =>
        response.json()
      )

    // Using Promise.all to wait for both requests to complete
    Promise.all([fetchData1(), fetchData2()])
      .then(([result1, result2]) => {
        setData1(result1)
        setData2(result2)
      })
      .catch((error) => {
        console.error('Error fetching data:', error)
      })
  }, []) // Empty dependency array means this effect runs once after the initial render

  // Render the component with the fetched data
  return (
    <div>
      <h1>Data from Endpoint 1:</h1>
      <pre>{JSON.stringify(data1, null, 2)}</pre>

      <h1>Data from Endpoint 2:</h1>
      <pre>{JSON.stringify(data2, null, 2)}</pre>
    </div>
  )
}

export default MyComponent

In this example, the useEffect hook is used to initiate the asynchronous requests when the component mounts. The Promise.all() is employed to wait for both requests to complete, and once they do, the state is updated with the received data. The component then renders the fetched data.

Demos:


You can utilize Promise.all to execute requests for managing relationships for both lists and details simultaneously. Below are examples demonstrating how to retrieve user lists along with their respective posts, combine them, and manipulate the resulting JSON data output.

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

function YourComponent() {
  const [data1, setData1] = useState(null)

  useEffect(() => {
    const fetchData = async () => {
      try {
        // Fetch users
        const usersResponse = await fetch(
          'https://jsonplaceholder.typicode.com/users'
        )
        const users = await usersResponse.json()

        // Fetch posts for each user
        const usersWithPosts = await Promise.all(
          users.map(async (user) => {
            const postsResponse = await fetch(
              `https://jsonplaceholder.typicode.com/posts?userId=${user.id}`
            )
            const posts = await postsResponse.json()
            return { ...user, posts }
          })
        )

        setData1(usersWithPosts)
      } catch (error) {
        console.error('Error fetching data:', error)
      }
    }

    fetchData()
  }, [])

  return (
    <div>
      {/* Render data1 here */}
      <pre>{JSON.stringify(data1, null, 2)}</pre>
    </div>
  )
}

export default YourComponent

Demos:

The Promise.race() function

Consider a scenario where you want to fetch data from two different endpoints, but you only need the result of the first resolved promise. In this case, you can use Promise.race(). Here's an example:

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

const MyComponent = () => {
  const [data, setData] = useState(null)

  useEffect(() => {
    const fetchData1 = () =>
      fetch('https://jsonplaceholder.typicode.com/users').then((response) =>
        response.json()
      )
    const fetchData2 = () =>
      fetch('https://jsonplaceholder.typicode.com/posts').then((response) =>
        response.json()
      )

    // Using Promise.race to get the result of the first resolved promise
    Promise.race([fetchData1(), fetchData2()])
      .then((result) => {
        setData(result)
      })
      .catch((error) => {
        console.error('Error fetching data:', error)
      })
  }, []) // Empty dependency array means this effect runs once after the initial render

  // Render the component with the fetched data
  return (
    <div>
      <h1>First Resolved Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}

export default MyComponent

In this example, Promise.race() is used to fetch data from two different endpoints, and the component will display the result of the first resolved promise. Keep in mind that Promise.race() is useful when you want to use the result of the first completed asynchronous operation. If the first request resolves successfully, it sets the state with that result. If there's an error in the first resolved promise, it will catch the error and handle it.


I hope this helps! Let me know if you have any questions.

Topics

Recent Blog List Content:

Archive