How to populate an array before conditional render which is based on the length of the array?

Viewed 112

I want to populate an array before render, and the render is conditional based on the length of the array.

If the length of the array > 0, do the render, otherwise don't.

But it seems that the length is always 0 becasue useEffect is asynchronous, so the render is never done.

Below is my code:

function OrderDetail(props) {
    const {userInfo} = useSelector(state => state.user)
    const [order, setOrder] = useState({});
    const [items, setItems] = useState([]);

    useEffect(()=>{
        if(!userInfo){
            props.history.push("/")
            return
        }

        const fetchOrderAndItems = async () => {
            // Get an order from backend.
            const order = await axios.get("/api/orders/" + props.match.params.id, {
                headers:{
                    authorization: "Bearer " + userInfo.token
                }
            });
            const order_data = order.data
            setOrder(order_data)

            // Take out the id of all items in the order, retrieve each product from backend, 
            //push them in "items_array" and set it in the "items" state.
            let items_array = [];
            order_data.orderItems.forEach( async (orderItem) => {
                const {data} = await axios.get("/api/products/" + orderItem.productId);
                items_array.push(data);
            })

            console.log("items_array.length: " + items_array.length);
            console.log(items_array);

            setItems(items_array);
        }

        fetchOrderAndItems();
    }, [userInfo]);

    // For each item in the "items" state, return a <tr> element.
    const itemsHtml = items.map(item => {
        return <tr key={item._id}>
             Something to display.
            </tr>
    })

    // if itemsHtml.length  > 0, render {itemsHtml}, otherwise display "Your cart is empty.".
    // But what is displayed is always "Your cart is empty."
    return <table className="table text-center">
    <thead style={{border: "none"}}>
    <tr>
        <th style={{border: "none"}} scope="col"></th>
        <th style={{border: "none"}} scope="col">Name</th>
        <th style={{border: "none"}} scope="col">Price</th>
        <th style={{border: "none"}} scope="col">Qty</th>
        <th style={{border: "none"}} scope="col">Subtotal</th>
        <th style={{border: "none"}} scope="col"></th>
    </tr>
    </thead>
    {
        itemsHtml.length > 0?
            <tbody>
                {itemsHtml}
            </tbody>
            :
            <tbody>
            <tr className="text-center">
                <th className="text-center" scope="row" colSpan="6">Your cart is empty. </th>
            </tr>
            </tbody>
    }
</table>

The output of the 2 lines of "console.log" is: enter image description here

We can see that the length of items_array is 0 while there is content in it, and I think it is because it is asynchronous.

And I guess because the length is 0, so the conditional render of itemsHtml.length > 0? is never true, so the desired content is never rendered.

Could anyone teach me how to solve it?

Thanks

1 Answers

Instead of for-each'ing over the items you should map the item ids to an array of GET requests and Promise.all them. The forEach doesn't wait for the callbacks to complete, this is why the array is empty when you log it but become populated by the time the log is displayed.

Promise.all will wait for all mapped requests to resolve before it resolves with an array of resolved item values.

Promise.all(
  order_data.orderItems.map(
    ({ productId }) => axios.get(`/api/products/${productId}`)
  ))
  .then(results => setItems(results));
Related