daniel.tosaba
daniel.tosaba

Reputation: 2563

nested async - best practices

I am making dummy app to test server side API. First request returns nested JSON object with Product names and number of variants that it has. From there I extract Product name so I can send second request to fetch list of variants with product images, sizes etc.

first request

second request

Sometimes it will load and display variants from only one product but most of the times it will work correctly and load all variants from both dummy products. Is there a better way of doing this to ensure it works consistently good. Also I would like to know if there is a better overall approach to write something like this.

Here is the code:

  import React, { useEffect, useState } from "react";
import axios from "axios";

import ShirtList from "../components/ShirtList";

const recipeId = "15f09b5f-7a5c-458e-9c41-f09d6485940e";

const HomePage = props => {
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    axios
      .get(
        `https://api.print.io/api/v/5/source/api/prpproducts/?recipeid=${recipeId}&page=1`
      )
      .then(response => {
        let shirtList = [];
        const itemsLength = response.data.Products.length;
        response.data.Products.forEach((element, index) => {
          axios
            .get(
              `https://api.print.io/api/v/5/source/api/prpvariants/?recipeid=${recipeId}&page=1&productName=${element.ProductName}`
            )
            .then(response => {
              shirtList.push(response.data.Variants);
              if (index === itemsLength - 1) {
                setLoaded(shirtList);
              }
            });
        });
      });
  }, []);

  const ListItems = props => {
    if (props.loaded) {
      return loaded.map(item => <ShirtList items={item} />);
    } else {
      return null;
    }
  };

  return (
    <div>
      <ListItems loaded={loaded} />
    </div>
  );
};

export default HomePage;

Upvotes: 0

Views: 508

Answers (2)

Asaf Aviv
Asaf Aviv

Reputation: 11770

You are setting the loaded shirts after each iteration so you will only get the last resolved promise data, instead fetch all the data and then update the state.

Also, separate your state, one for the loading state and one for the data.

Option 1 using async/await

const recipeId = '15f09b5f-7a5c-458e-9c41-f09d6485940e'
const BASE_URL = 'https://api.print.io/api/v/5/source/api'

const fetchProducts = async () => {
  const { data } = await axios.get(`${BASE_URL}/prpproducts/?recipeid=${recipeId}&page=1`)
  return data.Products
}

const fetchShirts = async productName => {
  const { data } = await axios.get(
    `${BASE_URL}/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`,
  )
  return data.Variants
}

const HomePage = props => {
  const [isLoading, setIsLoading] = useState(false)
  const [shirtList, setShirtList] = useState([])

  useEffect(() => {
    setIsLoading(true)

    const fetchProductShirts = async () => {
      const products = await fetchProducts()
      const shirts = await Promise.all(
        products.map(({ productName }) => fetchShirts(productName)),
      )
      setShirtList(shirts)
      setIsLoading(false)
    }

    fetchProductShirts().catch(console.log)
  }, [])
}

Option 2 using raw promises

const recipeId = '15f09b5f-7a5c-458e-9c41-f09d6485940e'
const BASE_URL = 'https://api.print.io/api/v/5/source/api'

const fetchProducts = () =>
  axios.get(`${BASE_URL}/prpproducts/?recipeid=${recipeId}&page=1`)
    .then(({ data }) => data.Products)


const fetchShirts = productName =>
  axios
    .get(
      `${BASE_URL}/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`,
    )
    .then(({ data }) => data.Variants)

const HomePage = props => {
  const [isLoading, setIsLoading] = useState(false)
  const [shirtList, setShirtList] = useState([])

  useEffect(() => {
    setIsLoading(true)

    fetchProducts
      .then(products) =>
        Promise.all(products.map(({ productName }) => fetchShirts(productName))),
      )
      .then(setShirtList)
      .catch(console.log)
      .finally(() => setIsLoading(false)
  }, [])
}

Now you have isLoading state for the loading state and shirtList for the data, you can render based on that like this

return (
  <div>
    {isLoading ? (
      <span>loading...</span>
      ) : (
      // always set a unique key when rendering a list.
      // also rethink the prop names
      shirtList.map(shirt => <ShirtList key={shirt.id} items={shirt} />)
    )}
  </div>
)

Refferences

Promise.all

Promise.prototype.finally

React key prop

Upvotes: 2

Ben Aston
Ben Aston

Reputation: 55739

The following should pass a flat array of all variants (for all products ) into setLoaded. I think this is what you want.

Once all the products have been retrieved, we map them to an array of promises for fetching the variants.

We use Promise.allSettled to wait for all the variants to be retrieved, and then we flatten the result into a single array.

useEffect(()=>(async()=>{
        const ps = await getProducts(recipeId)
        const variants = takeSuccessful(
            await Promise.allSettled(
                ps.map(({ProductName})=>getVariants({ recipeId, ProductName }))))
        setLoaded(variants.flat())
    })())

...and you will need utility functions something like these:

const takeSuccessful = (settledResponses)=>settledResponses.map(({status, value})=>status === 'fulfilled' && value)
const productURL = (recipeId)=>`https://api.print.io/api/v/5/source/api/prpproducts/?recipeid=${recipeId}&page=1`
const variantsURL = ({recipeId, productName})=>`https://api.print.io/api/v/5/source/api/prpvariants/?recipeid=${recipeId}&page=1&productName=${productName}`

const getProducts = async(recipeId)=>
    (await axios.get(productURL(recipeId)))?.data?.Products

const getVariants = async({recipeId, productName})=>
    (await axios.get(variantsURL({recipeId,productName})))?.data?.Variants

Upvotes: 1

Related Questions