17

So, I know this question has been asked 100's of times, but none of the solutions seems to work in my instance.

I am using useState hook to update state to a value initialValues that gets data returned from a getInitialValues function

const [initialValues, setInitialValues] = useState(getInitialValues());

The getInitialValues function does a logic check and either returns an object or another function retrieveDetails()

const getInitialValues = () => {
   let details;
   if(!addressDetails) {
      details = retrieveDetails();
   } else {
      details = { 
         ...,
         ...,
         ...
      };
   }
   return details;
}

The function, retrieveDetails is an async function that makes an API call, and I await the response and return the object received from the response.

const retrieveDetails = async () => {
   const addr = addressDetails[currentAddress];
   const { addressLookup } = addr;
   const key = process.env.API_KEY;
   const query = `?Key=${key}&Id=${addressLookup}`;
   const addrDetails = await new AddrService().getAddressDetails(query);
   return addrDetails;
}

However, when I log the state initialValues it returns Promise {<pending>}?

Even removing the API call and simple returning an object in it's place renders the same result.

Not sure the best way around this to actually return the object?

Any help would be greatly appreciated.

mcclosa
  • 943
  • 7
  • 29
  • 59
  • You can use [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html) to solve exactly this. See [traditional approaches](https://reactjs.org/docs/concurrent-mode-suspense.html#traditional-approaches-vs-suspense) for what to do without it. – Benjamin Gruenbaum Feb 24 '20 at 12:51
  • "The getInitialValues function does a logic check and either returns an object or another function retrieveDetails()" - no it doesn't. – Roamer-1888 Feb 24 '20 at 13:29
  • Does this answer your question? [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Henry Henrinson Sep 07 '20 at 14:45
  • Sidequest: why people doing if (!x) { a() }else{ b() } instead of if (x){ b() }else{ a() } I will never understand ... there is more ... but I'll spare you :) – stopsopa Jul 10 '23 at 23:35

5 Answers5

33

I don't think there is a way to get initial data to useState asynchronously, at least not yet.

React is not waiting for your data to arrive, the function will keep on running to completion while your async operation is queued (on the event loop side).

The current idiomatic way is to fetch the data in an effect and update the state.

useEffect(() => {
  getData(someParam).then(data => setState(data))
}, [someParam]) 

You can read more about it in the DOCS

Sagiv b.g
  • 30,379
  • 9
  • 68
  • 99
  • ...and then you get that developer console warning about trying to set state in an unmounted component. – Simon B. Jul 21 '20 at 18:52
  • 1
    @SimonB. Actually you will get that mostly if you don't handle a cancel or ignore on unmount. I wrote about it, you might find it useful. https://debuggr.io/react-update-unmounted-component – Sagiv b.g Jul 21 '20 at 19:46
  • Nice Sagiv! But going the whole extra mile with useReducer and in many use cases adding a separate useEffect only to avoid that warning can cause a lot of duplicate code. What do you think of my alternate answer below? :) – Simon B. Jul 21 '20 at 20:05
  • @SimonB. Well this is out of scope for this question, i did mention similar solutions in my post though i prefer the "abortable" useEffect – Sagiv b.g Jul 21 '20 at 22:24
  • Thanks! Works on Dropzone after Promise.all as no button is required – Hong Ly Sep 28 '21 at 07:45
  • @SimonB. avoiding that error is as simple as adding a return to the useEffect; I tried updating the answer to reflect this best practice but "the suggested edit queue is full"... So here ya go: `useEffect(() => { isLoading = true; getData(someParam).then(data => { if (isLoading) { setState(data)) } }) return() => { isLoading = false } }, [someParam]);` Sorry about the horrid formatting. Also recommend [looking at this comment from Dan Abramov (one of the React core maintainers)](https://github.com/facebook/react/issues/14326#issuecomment-441680293). – Gifford N. Jul 17 '22 at 04:41
1

So first things first: React is a library which is responsible for rendering DOM in any given time based on underlying state.

I assume that you will want to call your function getInitialValues() right after rendering your component, as a part of the logic of the component.

Then I would suggest to thing about this this way:

  1. You start rendering your component
  2. then component supposed to render some pending state of view
  3. you trigger your async function getInitialValues()
  4. once it finishes it's logic and get the result then you render finished state

so based on above logic I would suggest to do something like this:


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

function MyParentComponent() {

    const [state, setState] = useState(/* implicit undefined */);

    // trigger getInitialValues only on component init using useEffect
    useEffect(() => {
       (async function () {
           const data = await getInitialValues();
           
           setState(data);
       }())
    }, []); 

    // i got my stuff and I render it whatever it is
    if (state) {
        <pre>{JSON.stringify(state, null, 4)}</div>
    }

    // data still not arrived so let's render loader
    return <div>loading...</div>
}

stopsopa
  • 408
  • 4
  • 20
0

Asynchronous function used to give the Promise which gives you the data only when it gets fulfilled until then it will show you pending state.

Suppose if you want to get the user one by one by their id and have to store them in the react array state, then chances are there that you might no get the data of all the users.

So for that you can use the Promise.all() function to achieve the desire result in the React useEffect and you will get complete resolved promise data in react useState. For Example -

const [userList, setUserList] = useState([]);

useEffect(() => {

  let userIds = [1, 2, 3];

  let userPromises = userIds.map(e => {
     return fetch('/api/get/user_by_id/'+e).then(response => {
        return response.json();
     })
  });

  Promise.all(userPromises).then((userDataList) =>{ 
    setUserList(userDataList)
  });

}, [])
Aamir Khan
  • 111
  • 4
  • Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! Also I fail to see how this relates to the code in the OP – Bergi Jun 23 '23 at 07:37
  • 1
    @Bergi Thanks for pointing out. I have removed the Promise constructor anti pattern and this answer is just to give an idea how to receive and set data from multiple promises in react state. – Aamir Khan Jun 23 '23 at 09:24
-1

This isn't something React's built-in hooks support.

You need to build or import a custom hook.

Want short and simple? Try this:

const addressDetails = useAsyncFunctionResult(retrieveDetails);

and add this in hooks/useAsyncFunctionResults.js

function useAsyncFunctionResult(asyncFunction, dependencies = []) {
  const [result, setResult] = React.useState();

  React.useEffect(() => {
    let mounted = true;
    asyncFunction().then((data) => mounted && setResult(data))
    return () => { mounted = false; }
  }, dependencies]);

  return result;
}

Here the retrieveDetails function (the one from the question) will start executing (the ().then bit above) when the hook is first called. The result is kept until unmount. And we get no errors about changing component state after unmounting.

If you later want to add caching, use existing hooks instead if making your own.

There's no official useAsync in React, and likely never will be, because if your Promise did a request and the requesting component did unmount before the promise resolves, then best practice is to cancel which differs case by case.

Simon B.
  • 2,530
  • 24
  • 30
-2
const retrieveDetails = async () => {
   const addr = addressDetails[currentAddress];
   const { addressLookup } = addr;
   const key = process.env.API_KEY;
   const query = `?Key=${key}&Id=${addressLookup}`;
   const addrDetails = await Promise.resolve(new AddrService().getAddressDetails(query))
   return addrDetails;
}


**try this once changed the function a bit**
warmachine
  • 376
  • 1
  • 8