React - 将上下文传递给 SweetAlert 弹出窗口

React - pass context to SweetAlert popup

我的上下文如下:

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;

在组件 ShoppingCart 中,我使用了另一个组件 ShoppingCartQuantity,它又利用了上下文。它可以正常工作。

这是 ShoppingCartQuantity 组件:

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;

现在我尝试在 Homepage 组件中使用 ShoppingCartQuantity 组件,它是一个路由元素(参考 App.js),但出现错误 Uncaught TypeError: Cannot destructure property 'cart' of '(0 , react__WEBPACK_IMPORTED_MODULE_0__.useContext)(...)' as it is undefined.

因此上下文适用于路由器外部的组件,但不适用于路由器内部的组件。如果我将路由器包装在提供者中,难道不是所有的路由元素都可以访问上下文,还是我遗漏了什么?

更新

正如用户 Build Though 在评论中建议的那样,我尝试在另一个路由元素中使用 ShoppingCartQuantity 组件,它工作正常;所以问题不在于路由器!

下面是我在 Homepage 组件中使用 ShoppingCartQuantity 组件的代码:

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;

我在 SweetAlert 弹出窗口中使用该组件。我猜是 SweetAlert 组件无法访问上下文。有谁知道如何将上下文传递给 SweetAlert 组件?

更新 2

除 1 个小问题外,公认的解决方案效果很好:ShoppingCartQuantity 组件未在 SweetAlert 弹出窗口中重新呈现,并且 qty 不会在视觉上发生变化。

我使用 qty 作为 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>
    )
}

您没有显示您在何处使用 ShoppingCart 组件或 ShoppingCartQuantity 组件。 无论如何,当你声明一个路由时,你必须传递组件,而不是根元素。所以,这一行: <Route exact path="/" element={<Homepage/>}/> 一定是 <Route exact path="/" component={Homepage}/>

问题

甜蜜警报组件很可能在您的应用 外部 呈现,因此在 CartContextProvider 提供商外部呈现。我只是搜索了 repo 文档是否有指定根元素的方法,但这似乎不可能,因为这个甜蜜的警报代码不是特定于 React 的。

关于在警报中访问 Redux 上下文,请参阅其他类似的 issue

解决方案

ATM 似乎无法从模式中访问上下文值,因此恕我直言,解决方法可能是将您的 ShoppingCartQuantity 组件重构为包装器容器组件以访问上下文和表示组件接收上下文值和任何回调。

我还建议将您想要的数量 increment/decrement 数量传递给 updateCart 而不是传递 "+"/"-" 字符串和运算符比较。

示例:

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;

在您的应用中 ShoppingCartQuantity 组件在 CartContextProvider 中使用的地方用 withShoppingCartContext HOC 装饰它并正常使用。

购物车

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

const ShoppingCartQuantity = withShoppingCartContext(ShoppingCartQuantityBase);

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

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

在使用 ShoppingCartQuantity 组件的地方 上下文之外,就像在 sweet modal 中一样,在 React 代码中访问上下文并传入上下文值和回调。

...
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;

其他问题

您的上下文提供程序在更新数量时正在改变状态。更新嵌套状态时,您仍应创建正在更新的数组元素的浅表副本。

示例:

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>
  );
};