'React - pass context to SweetAlert popup

My context is as follows:

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

export const CartContext = createContext();

const CartContextProvider = (props) => {
    const [cart, setCart] = useState(JSON.parse(localStorage.getItem('cart')) || []);

    useEffect(() => {
        localStorage.setItem('cart', JSON.stringify(cart));
    }, [cart]);

    const updateCart = (productId, op) => {
        let updatedCart = [...cart];

        if (updatedCart.find(item => item.id === productId)) {
            let objIndex = updatedCart.findIndex((item => item.id === productId));

            if (op === '-' && updatedCart[objIndex].qty > 1) {
                updatedCart[objIndex].qty -= 1;
            } else if (op === '+') {
                updatedCart[objIndex].qty += 1;
            }
        } else {
            updatedCart.push({id: productId, qty: 1})
        }

        setCart(updatedCart);
    }

    const removeItem = (id) => {
        setCart(cart.filter(item => item.id !== id));
    };

    return (
        <CartContext.Provider value={{cart, updateCart, removeItem}}>
            {props.children}
        </CartContext.Provider>
    )
};

export default CartContextProvider;

App.js:

import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import NavigationBar from "./components/layout/navigationBar/NavigationBar";
import Homepage from "./pages/homepage/Homepage";
import AboutUsPage from "./pages/aboutUs/AboutUsPage";
import ContactPage from "./pages/contact/ContactPage";
import SearchPage from "./pages/search/SearchPage";
import ShoppingCart from "./components/layout/shoppingCart/ShoppingCart";
import CartContextProvider from "./context/CartContext";

function App() {
    return (
        <div>
            <CartContextProvider>
                <Router>
                    <NavigationBar/>
                    <ShoppingCart/>
                    <Routes>
                        <Route exact path="/" element={<Homepage/>}/>
                        <Route path="/a-propos" element={<AboutUsPage/>} />
                        <Route path="/contact" element={<ContactPage/>}/>
                        <Route path="/recherche" element={<SearchPage/>}/>
                    </Routes>
                </Router>
            </CartContextProvider>
        </div>
    );
}

export default App;

In the component ShoppingCart I am using another component ShoppingCartQuantity which in turn makes use of the context. It works as it should.

Here's the ShoppingCartQuantity component:

import React, {useContext} from "react";
import {CartContext} from "../../../context/CartContext";

import styles from './ShoppingCartQuantity.module.css'

const ShoppingCartQuantity = ({productId}) => {
    const {cart, updateCart} = useContext(CartContext);

    let qty = 0;
    if (cart.find((item => item.id === productId))) {
        let objIndex = cart.findIndex((item => item.id === productId));

        qty = cart[objIndex].qty;
    }

    return (
        <div>
            <span>
                <span className={`${styles.op} ${styles.decrementBtn}`} onClick={() => updateCart(productId, '-')}>-</span>
                <span className={styles.qty}>{qty}</span>
                <span className={`${styles.op} ${styles.incrementBtn}`} onClick={() => updateCart(productId, '+')}>+</span>
            </span>
        </div>
    )
}

export default ShoppingCartQuantity;

Now I am trying to use the ShoppingCartQuantity component in the Homepage component which is a route element (refer to App.js) but getting the error Uncaught TypeError: Cannot destructure property 'cart' of '(0 , react__WEBPACK_IMPORTED_MODULE_0__.useContext)(...)' as it is undefined.

So the context is working for components outside the router but not for those inside it. If I have wrapped the router within the provider, shouldn't all the route elements get access to the context or am I missing something?

UPDATE

As user Build Though suggested in the comments, I tried using the ShoppingCartQuantity component in another route element and it works fine; so the problem is not with the router!

Below is the code of how I am using the ShoppingCartQuantity component in the Homepage component:

import React, { useState, useEffect,  useRef } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import Subcat from "../../components/subcat/Subcat";
import CategoryService from "../../services/api/Category";
import SubCategoryService from "../../services/api/SubCategory";
import CategoriesLayout from "../../utils/CategoriesLayout";
import CategoryCard from "../../components/category/CategoryCard";
import { Triangle } from  'react-loader-spinner'
import ScrollIntoView from 'react-scroll-into-view'
import ProductService from "../../services/api/Product";
import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content';
import YouTube from 'react-youtube';
import FavoriteBtn from "../../components/favorite/FavoriteBtn";
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";

import "./Homepage.css";
import "../../components/product/ProductModal.css"
import "react-loader-spinner";
import modalStyles from "../../components/product/ProductModal.module.css"

function Homepage() {
    const [categories, setCategories] = useState([]);
    const [subCats, setSubCats] = useState([]);
    const [loader, setLoader] = useState(false);
    const ResponsiveGridLayout = WidthProvider(Responsive);
    const scrollRef = useRef();
    const productModal = withReactContent(Swal);
    const opts = {
        // height: '390',
        // width: '640',
        playerVars: {
            autoplay: 1,
        }
    };

    useEffect(() => {
        CategoryService.get().then((response) => {
            setCategories(response);
        });
    }, []);

    function showSubCatsHandler(catId) {
        setLoader(true);
        setSubCats([]);
        SubCategoryService.get(catId).then((response) => {
            setSubCats(response.data);
            setLoader(false);
            scrollRef.current.scrollIntoView({ behavior: "smooth" });
        });
    }

    function showProductPopupHandler(productId) {
        ProductService.get(productId).then((response) => {
            const product = response.data;

            return productModal.fire({
                html:
                    <div>
                        <h3 className={modalStyles.header}>{product.AMP_Title}</h3>
                        <h4 className={`${modalStyles.price} ${modalStyles.header}`}>{"CHf " + product.AMP_Price}</h4>
                        <img className={modalStyles.image} src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/products/' + product.AMP_Image} />
                        {
                            product.descriptions.map((desc, _) => (
                                <div key={desc.AMPD_GUID}>
                                    {
                                        desc.AMPD_Title === '1' && <h4 className={modalStyles.header}>{product.AMP_Title}</h4>
                                    }
                                    {
                                        desc.AMPD_Image !== '' && <img src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/descriptions/' + desc.AMPD_Image} className={desc.AMPD_Alignment === 'left' ? modalStyles.descImageLeft : modalStyles.descImageRight} />
                                    }
                                    <p className={modalStyles.description}>{desc.AMPD_Description}</p>
                                </div>
                            ))
                        }
                        <br/>
                        <div>
                            <FavoriteBtn productId={product.AMP_GUID}/>
                            <ShoppingCartQuantity productId={product.AMP_GUID} />                          
                        </div>
                        <br/>
                        {
                            product.AMP_VideoId !== '' &&
                            <YouTube
                                videoId={product.AMP_VideoId}
                                opts={opts}
                            />
                        }
                    </div>,
                showConfirmButton: false,
                showCloseButton: true
            });
        });
    }

    return (
        <div>
            <div className="categories-container">
                <ResponsiveGridLayout
                    className="layout"
                    layouts={ CategoriesLayout }
                    breakpoints={ { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 } }
                    cols={ { lg: 8, md: 8, sm: 6, xs: 4, xxs: 2 } }
                    isDraggable={ false }
                >
                    {
                        categories.map((cat, index) => (
                            <div key={index}>
                                <CategoryCard
                                    category_id = {cat.AMC_GUID}
                                    image = {cat.AMC_Image}
                                    showSubCatsHandler = {showSubCatsHandler}
                                />
                            </div>
                        ))
                    }
                </ResponsiveGridLayout>
                {
                    loader &&
                    <Triangle
                        height="100"
                        width="100"
                        color='#bcad70'
                        ariaLabel='loading'
                        wrapperClass="loader"
                    />
                }
                <div ref={scrollRef}>
                    {
                        Object.keys(subCats).map((keyName, _) => (
                            <Subcat
                                key={subCats[keyName].AMSC_GUID}
                                title={ subCats[keyName].AMSC_Title }
                                products={ subCats[keyName].products }
                                showProductPopupHandler = {showProductPopupHandler}
                            />
                        ))
                    }
                </div>
            </div>
        </div>
    );
}

export default Homepage;

I am using the component in a SweetAlert popup. I guess it's the SweetAlert component that is not getting access to the context. Does anyone have an idea how to pass the context to the SweetAlert component?

UPDATE 2

The accepted solution works great except for 1 small issue: the ShoppingCartQuantity component was not re-rendering inside the SweetAlert popup and the qty would not change visually.

I updated the component by using the qty as a state.

const ShoppingCartQuantity = ({ qty, productId, updateCart }) => {
    const [quantity, setQuantity] = useState(qty);

    const updateCartHandler = (productId, amount) => {
        updateCart(productId, amount);
        setQuantity(Math.max(quantity + amount, 1));
    }

    return (
        <div>
            <span>
                <span
                    className={`${styles.op} ${styles.decrementBtn}`}
                    onClick={() => updateCartHandler(productId, -1)}
                >
                  -
                </span>
                <span className={styles.qty}>{quantity}</span>
                <span
                    className={`${styles.op} ${styles.incrementBtn}`}
                    onClick={() => updateCartHandler(productId, 1)}
                >
                  +
                </span>
            </span>
        </div>
    )
}


Solution 1:[1]

Issue

It's very likely that the sweet alert component is rendered outside your app, and thus, outside the CartContextProvider provider. I just searched the repo docs if there is a way to specify a root element, but this doesn't seem possible since this sweet alert code isn't React specific.

See this other similar issue regarding accessing a Redux context in the alert.

Solution

It doesn't seem possible ATM to access the context value from within the modal, so IMHO a workaround could be to refactor your ShoppingCartQuantity component into a wrapper container component to access the context and a presentation component to receive the context values and any callbacks.

I suggest also just passing the amount you want to increment/decrement the quantity by to updateCart instead of passing a "+"/"-" string and operator comparison.

Example:

export const withShoppingCartContext = Component => props => {
  const { cart, removeItem, updateCart } = useContext(CartContext);
  return <Component {...props} {...{ cart, removeItem, updateCart }} />;
}

const ShoppingCartQuantity = ({ cart, productId, updateCart }) => {
  const qty = cart.find(item => item.id === productId)?.qty ?? 0;

  return (
    <div>
      <span>
        <span
          className={`${styles.op} ${styles.decrementBtn}`}
          onClick={() => updateCart(productId, -1)}
        >
          -
        </span>
        <span className={styles.qty}>{qty}</span>
        <span
          className={`${styles.op} ${styles.incrementBtn}`}
          onClick={() => updateCart(productId, 1)}
        >
          +
        </span>
      </span>
    </div>
  )
}

export default ShoppingCartQuantity;

In places in your app where ShoppingCartQuantity component is used within the CartContextProvider decorate it with the withShoppingCartContext HOC and use normally.

ShoppingCart

import ShoppingCartQuantityBase, {
  withShoppingCartContext
} from "../../components/layout/shoppingCart/ShoppingCartQuantity";

const ShoppingCartQuantity = withShoppingCartContext(ShoppingCartQuantityBase);

const ShoppingCart = (props) => {
  ...

  return (
    ...
    <ShoppingCartQuantity productId={....} />
    ...
  );
};

In places where ShoppingCartQuantity component is used outside the context, like in the sweet modal, access the context within the React code and pass in the context values and callbacks.

...
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
...

function Homepage() {
  ...
  const { cart, updateCart } = useContext(CartContext);
  const productModal = withReactContent(Swal);
  ...

  function showProductPopupHandler(productId) {
    ProductService.get(productId)
      .then((response) => {
        const product = response.data;

        return productModal.fire({
          html:
            <div>
              ...
              <div>
                <FavoriteBtn productId={product.AMP_GUID}/>
                <ShoppingCartQuantity
                  productId={product.AMP_GUID}
                  {...{ cart, updateCart }}
                />                          
              </div>
              ...
            </div>,
          showConfirmButton: false,
          showCloseButton: true
        });
      });
  }

  return (...);
}

export default Homepage;

Additional Issues

Your context provider is mutating state when updating quantities. When updating nested state you should still create a shallow copy of the array elements that are being updated.

Example:

const CartContextProvider = (props) => {
  ...

  const updateCart = (productId, amount) => {
    // only update if item in cart
    if (cart.some(item => item.id === productId)) {
      // use functional state update to update from previous state
      // cart.map creates shallow copy of previous state
      setCart(cart => cart.map(item => item.id === productId
        ? {
          ...item, // copy item being updated into new object reference
          qty: Math.max(item.qty + amount, 1), // minimum quantity is 1
        }
        : item
      ));
    }
  }

  const removeItem = (id) => {
    setCart(cart => cart.filter(item => item.id !== id));
  };

  return (
    <CartContext.Provider value={{ cart, updateCart, removeItem }}>
      {props.children}
    </CartContext.Provider>
  );
};

Solution 2:[2]

You did't show where you are using the ShoppingCart component or the ShoppingCartQuantity component. Anyway, when you declare a route, you must pass the component, not the root element. So, this line: <Route exact path="/" element={<Homepage/>}/> must be <Route exact path="/" component={Homepage}/>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 AlbertoZ