React: Async useEffect is pretty much unreadable

Created on 26 Nov 2018  路  8Comments  路  Source: facebook/react

heh

The only alternative is using then() chains which aren't very readable at their own.

Most helpful comment

I think you're making it more complicated than it needs to be.

useEffect(() => {
  async function fetchMyAPI() {
    let url = 'http://something';
    let config = {};
    const response = await myFetch(url);
    console.log(response);
  }  

  fetchMyAPI();
}, []);

If it uses some prop like productId:

useEffect(() => {
  async function fetchMyAPI() {
    let url = 'http://something/' + productId;
    let config = {};
    const response = await myFetch(url);
    console.log(response);
  }  

  fetchMyAPI();
}, [productId]);

However, note that this doesn't guarantee requests come in order. So a better solution would be:

useEffect(() => {
  let didCancel = false;

  async function fetchMyAPI() {
    let url = 'http://something/' + productId;
    let config = {};
    const response = await myFetch(url);
    if (!didCancel) { // Ignore if we started fetching something else
      console.log(response);
    }
  }  

  fetchMyAPI();
  return () => { didCancel = true; }; // Remember if we start fetching something else
}, [productId]);

This is a good article that goes into more detail and has more useful examples: https://www.robinwieruch.de/react-hooks-fetch-data/

In longer term, we'll recommend Suspense for data fetching which will look more like

const response = MyAPIResource.read();

and no effects. But in the meantime the code above works fine.

All 8 comments

I think you're making it more complicated than it needs to be.

useEffect(() => {
  async function fetchMyAPI() {
    let url = 'http://something';
    let config = {};
    const response = await myFetch(url);
    console.log(response);
  }  

  fetchMyAPI();
}, []);

If it uses some prop like productId:

useEffect(() => {
  async function fetchMyAPI() {
    let url = 'http://something/' + productId;
    let config = {};
    const response = await myFetch(url);
    console.log(response);
  }  

  fetchMyAPI();
}, [productId]);

However, note that this doesn't guarantee requests come in order. So a better solution would be:

useEffect(() => {
  let didCancel = false;

  async function fetchMyAPI() {
    let url = 'http://something/' + productId;
    let config = {};
    const response = await myFetch(url);
    if (!didCancel) { // Ignore if we started fetching something else
      console.log(response);
    }
  }  

  fetchMyAPI();
  return () => { didCancel = true; }; // Remember if we start fetching something else
}, [productId]);

This is a good article that goes into more detail and has more useful examples: https://www.robinwieruch.de/react-hooks-fetch-data/

In longer term, we'll recommend Suspense for data fetching which will look more like

const response = MyAPIResource.read();

and no effects. But in the meantime the code above works fine.

What if I need to close over setSomething(value returned from async)?

// Elsewhere
async function fetchWidgets() {
  return await fetch('./api/widgets').then(r => r.json())
}

// In my component:
function MyComponent() {
  const [widgets, setWidgets] = useState([]);
  // Need another async wrapper function to close over `setWidgets`?
  useEffect(() => (async () => setWidgets(await fetchWidgets()))());

(Or based on your example):

function MyComponent() {
  const [widgets, setWidgets] = useState([]);
  async function unnecessaryApiWrapperClosureThatAddsLayersOfIndirection() {
     setWidgets(await fetchWidgets());
  }
  useEffect(() => unnecessaryApiWrapperClosureThatAddsLayersOfIndirection());

Also where do the errors go that are thrown in that async function invocation? I'd need to .catch() it? Or try/catch the body of the async so the promise always resolves?

useEffect(() => (async () => { 
  try { 
    setWidgets(await fetchWidgets());
  catch e {
    setError(e);
  }
})());

Why not make useEffect take an async function if we'd have to wrap them all the time anyways?

I guess one can always just then:

function MyComponent() {
  const [widgets, setWidgets] = useState([]);
  useEffect(() => { 
    fetchWidgets.then(w => setWidgets(w)) 
  });

@gaearon
now I should use useEffect instead of componentDidMount into a React Component with Hooks.

Scenario "initial unique call to a server":
To accomplished this, DependencyList ( second argument of useEffect) in useEffect should every time an empty array otherwise the application will send every state change a fetch call to the server.

it' means , this it the best practise to getData

    useEffect(() => {
        console.log("useEffect, fetchData here");
    }, []);
````

Full Code here
<details>

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

export function CaseAssignDropDown() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        console.log("useEffect, fetchData here");
    }, []);
    /*
    [] -> get data once
    [count] or removed [] -> get data each click
    */
    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
                </button>
        </div>
    );
}

https://stackoverflow.com/questions/54596192/react-hooks-async-best-practise

Please ask on StackOverflow and feel free to link to your question from here. We don鈥檛 use issues for support because answers get lost on GitHub.

componentDidMount supports being async so I think useEffect should accept a promise and automatically resolve it to avoid having to .then them.

@codeaid It is an intentional design decision. The async/await model is a pitfall for effects in general (and has always been a pitfall in classes!) due to race conditions. In the longer term, Suspense will be the recommended data fetching solution, and you can do async / await there. But Suspense for data fetching is not ready yet.

@lxe

As I explained above, setState(await something())) is a bad idea because you don't know whether await something() is still relevant. For example, if the user starts fetching one profile page but then switches to another profile. Then you're at the mercy of which request arrives first, and can show incorrect data.

This kind of code would handle it correctly:

useEffect(() => {
  let didCancel = false;

  async function fetchMyAPI() {
    let url = 'http://something/' + productId;
    let config = {};
    const response = await myFetch(url);
    if (!didCancel) { // Ignore if we started fetching something else
      console.log(response);
    }
  }  

  fetchMyAPI();
  return () => { didCancel = true; }; // Remember if we start fetching something else
}, [productId]);

I'm going to lock this issue as resolved to prevent further confusion. Here's a good article that answers your other questions (e.g. about loading states): https://www.robinwieruch.de/react-hooks-fetch-data/

Was this page helpful?
0 / 5 - 0 ratings