Search code examples
javascriptreactjsreact-hooksfrontend

Best practice for fetching API data asynchronously from different endpoints with React?


I am learning React for a project at work and currently really having trouble getting the hang of the asynchronous model.

Here is the component I'm working on:

const Customers = () => {
    const [logos, setLogos] = useState([]);
    const [customers, setCustomers] = useState([]);
    const [titlePrompt, setTitlePrompt] = useState([]);
    const navigate = useNavigate();

    useEffect(() => {
        // create a customer client object 
        const client = new Client();

        // query for the customer list and save name and token
        if (customers.length === 0) {
            client.get_customers().then((res) => {
                const customer_info = res.map(customer => {
                    return {
                        'name': customer.customer_name,
                        'token': customer.mqtt_customer_token
                    }
                });
                setCustomers(customer_info);
            });
        }

        if (customers.length > 0) {
            const fetched_logos = [];
            for (let i = 0; i < customers.length; i++) {
                client.get_site_by_type(customers[i].token, 1).then((res) => {
                    const new_logo = {
                        [customers[i].name]: res.logo 
                    };
                    fetched_logos.push(new_logo);
                });
            }
            setLogos(fetched_logos);
        }

        setTitlePrompt("Select A Customer Shown Below");
    }, [customers, logos]);

    /**
     * button handler to transition to a dataframe view of the chosen customer via token
     * @param {defining} token 
     */
    const onButtonClick = (token) => {
        navigate('/dataframe', { state: { token } });
    };

    return (
        <div className={'mainContainer'}>
            <div className={'contentContainer'}>
                <div className={'titleContainer'}>
                    <div>{titlePrompt}</div>
                </div>
                <br/>
                <ImageList cols={4}>
                    {customers.map(customer => (
                        <ImageListItem key={customer.name}>
                            <img alt={customer.name} src={`data:image/png;base64,${logos[customer.name]}`}></img>
                        </ImageListItem>
                    ))}
                </ImageList>
            </div>
        </div>
    );
};

export default Customers;

What I'm trying to do is fetch customer information such as name and logo and display them in an ImageList.

The problem is, first I have to fetch the name and token (used for subsequent API calls) for each customer from one database, then I need to fetch the logos one by one since they all reside in specific customer databases. I want to render the component with only the img component's alt text and then populate the images as they come in. I just can't figure out how to synchronize this correctly inside useEffect. I also don't know if using the if statements to control what gets executed during subsequent useEffect calls is the right thing to do or just hacky.

I tried chaining .then but the setState calls are asynchronous so it leads to race conditions and doesn't work.

Thank you for reading.

EDIT:

Here is a code snippet showing my attempt with chaining .then:

client.get_customers().then((res) => {
            const customer_info = res.map(customer => {
                return {
                    'name': customer.customer_name,
                    'token': customer.mqtt_customer_token
                }
            });
            setCustomers(customer_info);
        }).then(() => {
            for (let i = 0; i < customers.length; i++) {
                client.get_site_by_type(customers[i].token, 1).then((res) => {
                    const new_logo = {
                        [customers[i].name]: res[0].logo
                    }
                    setLogos([...logos, new_logo]);
                });
            }
        });

Solution

  • Two problems, one is that state variables do not update as soon as you call the set function. Better is to use the variable that contains the received data (here, I return customer_info so it can be used in the then, rather than calling setCustomers and then expecting the const customers to have been updated, which it will not have been).

    Second problem is that you need to use promise.all or similar to sync it properly.

    Untested illustrative code:

    client.get_customers().then((res) => {
      const customer_info = res.map(customer => {
        return {
          'name': customer.customer_name,
          'token': customer.mqtt_customer_token
        }
      });
      setCustomers(customer_info);
      return customer_info;
    }).then((customer_info) => {
      const promises = customer_info.map(customer => 
        client.get_site_by_type(customer.token, 1).then((res) => {
          const new_logo = {
            [customer.name]: res[0].logo
          }
          return new_logo;
        })
      );
      Promise.all(promises).then(logos => setLogos(logos));
    });