Search code examples
javascriptreactjsreact-hookses6-promise

React Hooks - Set State variable from Promise in another function


I'm trying not to write all my code in the same file, and to extract commonly used functions like downloading an image from Firebase to separate functions/files.

I have two functions in React named

  1. OfferCard (renders a card based on props)
  2. getImage (fetches an image's download-URL from Firebase Storage, to provide an image to the OfferCard component).

I'm trying to fetch an image file from getImage and display that image in the returned <CardMedia /> image property from the OfferCard functional component.

The problem I'm facing is, getImage doesn't fetch the data and return it in time for the setOfferImage. Meaning the state variable offerImage remains null or undefined.

In the console, I can see that the getImage is console logging the value AFTER the OfferCard console logs the value/.

I believe it's an issue with asynchronous code and promises but am not too sure how to resolve this one.

OfferCard.js:

import getImage from '../utils/helperFunctions/getImage';

export default function OfferCard(props) {
    const classes = useStyles();
    const [offerImage, setOfferImage] = React.useState(null)

    useEffect(() => {
        if (props.image) {
            initFirebase();
            setOfferImage(getImage(props.image));
            console.log(offerImage)
        }
    }, [])

    return (

        <Link href={`/offers/${props.id}`}>
            <Card className={classes.card} >
                {offerImage ? 
                    <CardMedia
                    image={offerImage}
                /> : <CircularProgress/>
                }
           </Card>
        </Link>
    )
}

getImage:

export default function getImage(path) {
    let storage = firebase.storage();
    let pathReference = storage.ref(`${path}.png`);

    pathReference.getDownloadURL()
        .then((url) => {
            console.log("returning", url);
            return (url);
        })
        .catch((error) => {
            // Handle any errors
            console.log("Could not get image: ", error);
        });
}


Solution

  • the problem in this is that your getImage function is actually a void function, as it will reach the end of the code without finding any return, as the return(url) is performed on a second moment. The solution is to make that function asynchronous, returning a Promise:

    export default async function getImage(path) {
      let storage = firebase.storage();
      let pathReference = storage.ref(`${path}.png`);
    
      return new Promise((resolve, reject) => {
        pathReference.getDownloadURL()
          .then((url) => {
            console.log("returning", url);
            resolve(url);
          })
          .catch((error) => {
            // Handle any errors
            reject(error)
            console.log("Could not get image: ", error);
          });
      })
    }
    

    So that you can then await it in your useEffect. Be aware you can't actually await inside an useEffect (learn more), so your OfferCard.js will become

    useEffect(() => {
      const getImageAsync = async () => {
        const url = await getImage(props.image)
        if (url)
          setOfferImage(url)
        else {
          // handle error
        }
      }
    
      if (props.image) {
        initFirebase();
        getImageAsync();
      }
    }, [])