Search code examples
reactjsreact-hooksuse-effectuse-context

Can we trigger parent component render when Context is updated from child component?


I have a child component which is a product item. I use a context in it to modify the json. when I modify the quantity of item (I'm using a json as a fake db for now). In a parent component, I need to update the new total quantity, price, and so on. So I subscribe useEffect() in this component to data changement, but it doesn't triggers. What am I doing wrong ?

cartAPIContext.js :

import { createContext, useEffect, useContext, useState, useMemo } from "react";


export const CartAPIContext = createContext();

const CartAPIContextProvider = ({ children }) => {
    const [data, setData] = useState([]);
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
        //On ira chercher le dernier panier actif de l'utilisateur
        let url = "/cartFakedb.json";

        fetch(url)
        .then(response => response.json())
        .then((data) => {
            setData(data);
            setIsLoading(false);
        })
        .catch((error) => console.log(error));
    }, [])

    const contextValue = useMemo(() => ({
        data,
        setData,
        isLoading
    }), [data, setData, isLoading])

    return (
        <CartAPIContext.Provider value={contextValue}>
            {children}
        </CartAPIContext.Provider>
    )
}


export default CartAPIContextProvider;

export function useCartAPI() {
    const context = useContext(CartAPIContext);
    if (context === undefined) {
        throw new Error("Context must be used within a Provider");
    }
    return context;
}

Child component :

import { forwardRef, useContext, useEffect, useState } from "react";
import { useCartAPI } from "./CartAPIContext";

const CartProductItem = ({ libraryId, product }) => {
    const { data, setData } = useCartAPI();

    const [ selectedItemQuantity, setSelectedItemQuantity ] = useState(product.quantity);

    const [ itemInStock, setItemInStock ] = useState(product.stock);
    const [ itemPrice, setItemPrice ] = useState((product.unitPrice*selectedItemQuantity).toFixed(2));

    function changeProductQuantity(event) {
        setSelectedItemQuantity(event.target.value);
    }

    useEffect(() => {
        for(let i = 0; i < data.libraries.length; i++) {
            if(data.libraries[i].id === libraryId) {
                for(let j = 0; j < data.libraries[i].products.length; j++) {
                    if(data.libraries[i].products[j].id === product.id) {
                        data.libraries[i].products[j].quantity = parseInt(selectedItemQuantity);
                    }
                }
            }
        }
        setData(data);
        setItemPrice((product.unitPrice*selectedItemQuantity).toFixed(2));
    }, [selectedItemQuantity]);

    console.log(data);

    return (
        <li className="item">
            <article>
                <div className="left">
                    <img src={product.cover_img} alt={product.title} loading="lazy" />
                    <select value={selectedItemQuantity} onChange={changeProductQuantity}>
                        {Array.from(Array(itemInStock)).map((empty, index) => 
                            <option key={product.id + index} value={index + 1}>{index + 1}</option>
                        )}
                    </select>
                </div>
            </article>
        </li>
    );
}

export default CartProductItem;

Parent Component :

import { useCartAPI } from "./CartAPIContext";
import CartProductList from "./CartProductList";
import { useEffect } from "react";

const CartLibraryItem = ({ library }) => {

    const { data, setData } = useCartAPI();

    function getTotalNumberOfArticles() {
        let total = {quantity: library.products[0].quantity};
        if (library.products.length > 1) {
            total = library.products.reduce((productA, productB) => {
                return { quantity: productA.quantity + productB.quantity };
            })
        }

        console.log(total);
        return total;
    }

    useEffect(() => {
        getTotalNumberOfArticles();
    }, [data]) //<---- I'm looking to trigger this useEffect(), when I use the select in the child component above.

    return (
        <li className="cart-library-item" key={library.id}>
            //I delete this part for space
        </li>
    );
}

export default CartLibraryItem;

Solution

  • data.libraries[i].products[j].quantity = parseInt(selectedItemQuantity);
    //...
    setData(data);
    

    You are mutating the state, and then setting state with the same object that's already in the state. React compares the old state with the new state, sees that they are the same object, and so it does not render.

    You need to create a new state instead. For such a deeply nested object this is a bit of a pain, but it will be something like this:

    useEffect(() => {
      const newData = {
        ...data,
        libraries: data.libraries.map(library => {
          if (library.id !== libraryId) {
            return library;
          }
          return {
            ...library,
            products: library.products.map(p => {
              if (p.id !== product.id) {
                return p;
              }
              return {
                ...p,
                quantity: parseInt(selectedItemQuantity)
              }
            });
          }
        })
      }
      setData(newData);
      setItemPrice((product.unitPrice*selectedItemQuantity).toFixed(2));
    }, [selectedItemQuantity]);