Search code examples
reactjsasync-awaitecmascript-2016

javascript async await with react. setState doesn't wait forawait


I am building a component tat get the members of a group and then download the image from the api. It uses async await in the componentDidMount and the photos come back correctly. however even though I am using async await. it doesn't wait for the images to resolve. Thus in setState it will return a promise.

my component:

import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUser } from "@fortawesome/free-solid-svg-icons";
import {
    GetGroupMembers,
    getImage
} from "../../../HttpRepositories/oneApiRepository";

class TeamCardPersonaMembers extends React.Component {
    constructor(props) {
        super(props);
        this._isMounted = false;
    }

state = {
    members: []
};

async componentDidMount() {
    this._isMounted = true;
    let members = await GetGroupMembers(this.props.teamId);

    const membersToSend = await members.map(async member => {
        const memberPhotoUrl = `${process.env.REACT_APP_ONE_API_URL}/api/users/${
            member.id
        }/photo`;

        let memberPhoto = await getImage(memberPhotoUrl);
        member.photo = memberPhoto;
        return member;
    });

    if (this.state.member !== "error" && this._isMounted) {
        this.setState({ members: membersToSend });
    }
}

render() {
    let members = [];

    console.log(this.state);

    if (this.state.members.length > 0) {
        this.state.members.map(member => {
            console.log("this is the member ", member);
            const memberImg = (
                <img
                    key={member.id}
                    src={member.photo}
                    alt={member.displayName}
                    className="team-card-persona-wrapper"
                    title={member.displayName}
                />
            );

            members.push(memberImg);
        });
    }

    return <React.Fragment>{members}</React.Fragment>;
}

componentWillUnmount() {
    this._isMounted = false;
}

}

console.log

enter image description here

export default TeamCardPersonaMembers;

any help or pointers will be greatly appreciated! Cheers!


Solution

  • The standard implementation of map does not await the callback passed to it. Resulting in the map call returning promises before the callbacks complete. You have two choices.

        const membersToSend = []
        for (const member of members) {
            const memberPhotoUrl = `${process.env.REACT_APP_ONE_API_URL}/api/users/${
                member.id
            }/photo`;
    
            let memberPhoto = await getImage(memberPhotoUrl);
            member.photo = memberPhoto;
            membersToSend.push(member);
        }
    
    

    or to roll your own implementation of async map.

    Array.prototype.mapAsync = async function (callback) {
      const output = []
      for (const el of this)
        output.push( await callback(el) ) <-- // Note the await on the callback
    
      return output
    }
    
    

    Edit: Note that the above method executes the promises in sequence. As Jed points out, if you want to fire all the requests at once and wait for them to resolve,

    const memberPhotos = await Promise.all(members.map(member => {
       const memberPhotoUrl = `${process.env.REACT_APP_ONE_API_URL}/api/users/${
                member.id
            }/photo`;
    
            return getImage(memberPhotoUrl);
    }))
    

    But this method could overload your network if there are too many requests, use with caution.