How to show values from array in content of SweetAlert2 [duplicate] - javascript

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

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

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}/>

Related

How to pass child prop to parent?

I've got some dynamic buttons that are a child component, and get assigned a value="URL" based off of my MongoDB. How do I go about passing that generated value to my parent/web component src={currentSrc}? When I assign it, it says that currentSrc is not defined?
Here's the SizeOptions:
import { useState } from "react";
export const SizeOptions = ({ size }) => {
const sizeName = Object.keys(size);
// Update Model Viewers Src
function changeSize (){
setSrc(currentSrc)
console.log(currentSrc)
}
if (!sizeName) return <></>;
return (
<div>
{size[sizeName].map((item) => (
<button key={item} className='size' value={item} onClick={changeSize} currentSrc={item}>
{sizeName}
</button>
))}
</div>
);
};
And here is the ProductScreen:
import './ProductScreen.css';
import { useEffect, useState } from "react";
//Components
import { SizeOptions } from '../components/SizeOptions';
const [currentSrc, setSrc] = useState(size[sizeName][0])
const ProductScreen = ({match}) => {
return(
<div className='sizebuttons'>
{product && (product.size || []).map((size, index) => (<SizeOptions key={index} size={size} setSrc={currentSrc} changeSize={changeSize}/>))}
</div>
<div className="productscreen__right">
<model-viewer
id="model-viewer"
src={currentSrc}
alt={product.productName}
ar
ar-modes="scene-viewer quick-look"
ar-placement="floor"
shadow-intensity="1"
camera-controls
min-camera-orbit={product.mincameraorbit}
max-camera-orbit={product.maxcameraorbit}
interaction-prompt="none">
<button slot="ar-button" className="ar-button">
View in your space
</button>
</model-viewer>
</div>
)}
The rendered button:
I have more code inside my ProductScreen, I just tried to keep it as minimized as possible to try and make it easier to help me figure it out! Any help would be greatly appreciated!
Good news! I finally figured it out after many, many attempts.
So in order to pass from child to parent this is what I came up with:
SizeOptions.js
export const SizeOptions = ({ size, changeSrc }) => {
const sizeName = Object.keys(size);
if (!sizeName) return <></>;
return (
<div>
{size[sizeName].map((url) => (
<button key={url} className='size' value={url} onClick={() => changeSrc(url)}>
{sizeName}
</button>
))}
</div>
);
};
export default SizeOptions;
ProductScreen.js
import './ProductScreen.css';
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
// Actions
import { getProductDetails } from "../redux/actions/productActions";
//Components
import { SizeOptions } from '../components/SizeOptions';
const ProductScreen = ({match}) => {
const dispatch = useDispatch();
const productDetails = useSelector(state => state.getProductDetails);
const { loading, error, product } = productDetails;
const [ src, setSrc ] = useState("Default URL")
return(
<div className='sizebuttons'>
{product && (product.size || []).map((size, index) => (<SizeOptions key={index} size= {size} changeSrc={src => setSrc(src)}/>))}
</div>
<model-viewer
id="model-viewer"
src={src}
alt={product.productName}
ar
ar-modes="scene-viewer quick-look"
ar-placement="floor"
shadow-intensity="1"
camera-controls
min-camera-orbit={product.mincameraorbit}
max-camera-orbit={product.maxcameraorbit}
interaction-prompt="none">
<button slot="ar-button" className="ar-button">
View in your space
</button>
</model-viewer>
)};
The only issue now is how to make the "Default URL" actually be a URL as right now if I put {src} there, it says it cannot do that before the component is rendered.

ReactJs: TypeError: Cannot assign to read only property 'cartHandler' of object '#<Object>'?

Here I'm trying to build a restaurant website. I already build half part of it. But I'm stuck at some point. I'm getting error. I can't find out in my code what is wrong. Whenever I tried to add new food by clicking add button Then I get the TypeError: Cannot assign to read-only property 'cartHandler' of object '#'?.. I tried to debug in cartHandler object but I'm getting the same errors many times. Can someone check it out, please?
Here is my App.js FIle
import logo from './logo.svg';
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
import './App.css';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css'
import Banner from './components/Banner/Banner';
import Header from './components/Header/Header';
import Foods from './components/Foods/Foods';
import Features from './components/Features/Features';
import Footer from './components/Footer/Footer';
// import SIgnUp from './components/SignUp/SIgnUp';
import NotFound from './components/NotFound/NotFound';
import FoodItemDetails from './components/FoodItemDetails/FoodItemDetails'
import { createContext, useContext, useState } from 'react';
import Login from './components/Login/Login';
export const userContext = createContext();
function App() {
const [cart , setCart] = useState([]);
const [orderId , setOrderId] = useState(null);
const [deliveryDetails , setDeliveryDetails] = useState({
todoor:null,road:null, flat:null, businessname:null, address: null
});
const [userEmail, setUserEmail] = useState(null);
const deliveryDetailsHandler = (data) => {
setDeliveryDetails(data)
}
const getUserEmail = (email) => {
setUserEmail(email);
}
const clearCart = () => {
const orderedItems = cart.map(cartItem => {
return {food_id : cartItem.id, quantity: cartItem.quantity}
})
const orderDetailsData = { userEmail , orderedItems, deliveryDetails }
fetch('https://red-onion-backend.herokuapp.com/submitorder' , {
method : "POST",
headers: {
"Content-type" : "application/json"
},
body : JSON.stringify(orderDetailsData)
})
.then(res => res.json())
.then(data=> setOrderId(data._id))
console.log(orderId);
setCart([])
}
const cartHandler = (data) => {
const alreadyAdded = cart.find(crt => crt.id == data.id );
const newCart = [...cart,data]
setCart(newCart);
if(alreadyAdded){
const reamingCarts = cart.filter(crt => cart.id != data);
setCart(reamingCarts);
}else{
const newCart = [...cart,data]
setCart(newCart);
}
}
const checkOutItemHandler = (productId, productQuantity) => {
const newCart = cart.map(item => {
if(item.id == productId){
item.quantity = productQuantity;
}
return item;
})
const filteredCart = newCart.filter(item => item.quantity > 0)
setCart(filteredCart)
}
const [logggedInUser, setLoggedInUser] = useState({});
const [signOutUser, setSignOutUser] = useState({});
return (
<userContext.Provider value={([logggedInUser, setLoggedInUser], [signOutUser, setSignOutUser])}>
<Router>
<div className="App">
<Switch>
<Route exact path="/">
<Header></Header>
<Banner></Banner>
<Foods></Foods>
<Features></Features>
<Footer></Footer>
</Route>
<Route path="/user">
<Login></Login>
</Route>
<Route path= "/food/:id">
<Header cart={cart}></Header>
{/* <FoodItemDetails cart={cart} cartHandler={cartHandler}></FoodItemDetails> */}
<FoodItemDetails cart={cart} cartHandler={cartHandler}></FoodItemDetails>
<Footer></Footer>
</Route>
<Route path ="*">
<NotFound></NotFound>
</Route>
</Switch>
</div>
</Router>
</userContext.Provider>
);
}
export default App;
Here is my FoodItemDetails.js File
import React, { useEffect, useState } from "react";
import { useParams } from "react-router";
// import { useParams } from "react-router";
// import PreLoader from "../PreLoader/PreLoader";
import { FontAwesomeIcon} from '#fortawesome/react-fontawesome'
import { faCartArrowDown, faCheckCircle } from '#fortawesome/free-solid-svg-icons'
import PreLoader from "../PreLoader/PreLoader";
const FoodItemDetails = (props) => {
// const {name,shortDescription,price,images} = props.food;
console.log(props, "nashir")
const [currentFood, setCurrentFood] = useState({});
const {id} = useParams();
console.log(id);
const [quantity, setQuantity] = useState(1);
const [isSuccess, setIsSuccess] = useState(false);
const [selectedBigImage, setSelectedBigImage] = useState(null);
const [preloaderVisibility, setPreloaderVisibility] = useState("block");
// const [quantity, setQuantity] = useState(1);
useEffect(() => {
fetch("https://red-onion-backend.herokuapp.com/food/" + id)
.then((res) => res.json())
.then((data) => {
setCurrentFood(data);
setPreloaderVisibility("none");
})
.catch((err) => console.log(err));
if (currentFood.images) {
setSelectedBigImage(currentFood.images[0]);
}
window.scrollTo(0, 0);
}, [currentFood.name]);
const finalCartHandler = (currentFood) => {
currentFood.quantity = quantity;
console.log(currentFood, 'food man');
props.cartHandler = currentFood;
setIsSuccess(true);
console.log(isSuccess, "what");
}
if(isSuccess){
setTimeout(() => {
setIsSuccess(false)
console.log(isSuccess);
}, 1500);
}
return (
<div className="food-details container my-5">
<PreLoader visibility={preloaderVisibility}></PreLoader>
{currentFood.name && (
<div className="row">
<div className="col-md-6 my-5">
<h1>{currentFood.name}</h1>
<p className="my-5">{currentFood.fullDescription}</p>
<div className="d-flex my-4">
<h2>${currentFood.price.toFixed(2)}</h2>
<div className="cart-controller ml-3">
<button
className="btn"
onClick={() => setQuantity(quantity <= 1 ? 1 : quantity - 1)}
>
-
</button>
<button
className="btn"
onClick={() => setQuantity(quantity + 1)}
>
+
</button>
</div>
</div>
<div className="action d-flex align-items-center">
<button className="btn btn-danger btn-rounded" onClick={() => finalCartHandler(currentFood)}><FontAwesomeIcon icon={faCartArrowDown} /> Add</button>
</div>
<div className="more-images mt-5">
{currentFood.images.map((img, index) => (
<img
onClick={() => setSelectedBigImage(currentFood.images[index])}
className={
currentFood.images[index] === selectedBigImage
? "mr-4 small-img active-small-img"
: "mr-4 small-img"
}
height="150px"
src={img}
alt=""
/>
))}
</div>
</div>
<div className="col-md-6 my-5">
<img src={selectedBigImage} className="img-fluid" alt="" />
</div>
</div>
)}
</div>
);
};
export default FoodItemDetails;
First of all you can not change property of props. They are read-only properties.
Second Your cartHnadler is function and not property but you are assigning value instead of calling function.
props.cartHandler = currentFood; // here you are assigning value
To solve this you need to call cartHandler and pass currentFood as argument like below:-
props.cartHandler(currentFood);
Props are a read-only property. Inside finalCartHandler, you are trying to assigning props.cartHandler to currentFood. I assume you're wanting to invoke props.cartHandler instead of assigning it.
I also notice inside cartHandler that you are checking if the id is the same with == which is a truthy/falsy equals, whereas === would work better (also in a few other spots with == and !=).

Why react loads images for many times? is it possible to save the first load?

to watch the problem you can vivsit the test site http://u100525.test-handyhost.ru/products
the problem appears if to click many times on category items, images of products start to bug becouse react loads image of one item over and over again, on every change of category - on every filter of products, so how to make one load and save somehow the loaded images?
so if i click on categories my code is filtering products array and update statement - visibleProducts then im doing visibleProducts.map((product)=>{});
and i`m getting bug problem, because every time when react renders my the component does request to the server for getting image by id and waits while the image will load, but if i click on an other category react(ProductItem) starts other request for new images then it is starting to bug they start blinking and changing ;c
im new in react and just stated to practice what i have to do guys?
is my code correct ?
here is my ProductItem component ->
import React, { useState, useEffect, memo, useCallback } from "react";
import { Link } from "react-router-dom";
import { connect } from "react-redux";
import { setModalShow, onQuickViewed, addedToCart } from "../../actions";
import Checked from "../checked";
import "./product-item.css";
import Spinner from "../spinner";
const ProductItem = ({
product,
wpApi,
addedToCart,
onQuickViewed,
setModalShow,
}) => {
const [prodImg, setProdImg] = useState("");
const [animated, setAnimated] = useState(false);
const [checked, setChecked] = useState(false);
const [itemLoading, setItemLoading] = useState(true);
const checkedFn = useCallback(() => {
setChecked(true);
setTimeout(() => {
setChecked(false);
}, 800);
},[product]);
const onModalOpen = useCallback((e, id) => {
onQuickViewed(e, id);
setModalShow(true);
}, product);
const addHandle = useCallback((e, id) => {
e.preventDefault();
addedToCart(id);
checkedFn();
},[product]);
useEffect(()=>{
setItemLoading(false);
}, [prodImg]);
useEffect(() => {
wpApi.getImageUrl(product.imageId).then((res) => {
setProdImg(res);
});
});
return (
<div className="product foo">
<div
className='product__inner'}
>
{!itemLoading? <div
className="pro__thumb"
style={{
backgroundImage:prodImg
? `url(${prodImg})`
: "assets/images/product/6.png",
}}
>
<Link
to={`/product-details/${product.id}`}
style={{ display: `block`, width: `100%`, paddingBottom: `100%` }}
>
</Link>
</div>: <Spinner/>}
<div className="product__hover__info">
<ul className="product__action">
<li>
<a
onClick={(e) => {
onModalOpen(e, product.id);
}}
title="Quick View"
className="quick-view modal-view detail-link"
href="#"
>
<span ><i class="zmdi zmdi-eye"></i></span>
</a>
</li>
<li>
<a
title="Add TO Cart"
href="#"
onClick={(e) => {
addHandle(e, product.id);
}}
>
{checked ? (
<Checked />
) : (
<span className="ti-shopping-cart"></span>
)}
</a>
</li>
</ul>
</div>
</div>
<div className="product__details">
<h2>
<Link to={`/product-details/${product.id}`}>{product.title}</Link>
</h2>
<ul className="product__price">
<li className="old__price">${product.price}</li>
</ul>
</div>
</div>
);
};
const mapStateToProps = ({ options, cart, total, showModal }) => {
return {};
};
const mapDispatchToProps = {
onQuickViewed,
setModalShow,
addedToCart,
};
export default connect(mapStateToProps, mapDispatchToProps)(memo(ProductItem));
here is my parent component Products ->
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import ProductItem from "../product-item";
import { withWpApiService } from "../hoc";
import { onQuickViewed, addedToCart, categoriesLoaded } from "../../actions";
import CategoryFilter from "../category-filter";
import Spinner from "../spinner";
import "./products.css";
const Products = ({
maxProducts,
WpApiService,
categoriesLoaded,
addedToCart,
onQuickViewed,
products,
categories,
loading,
}) => {
const [activeIndex, setActiveIndex] = useState(0);
const [activeCategory, setActiveCategory] = useState(0);
const [visibleProducts, setVisibleProducts] = useState([]);
const wpApi = new WpApiService();
useEffect(() => {
updateVisibleProducts(activeCategory, products);
}, [products]);
useEffect(() => {
wpApi.getCategories().then((res) => {
categoriesLoaded(res);
});
}, []);
const getCatId = (cat) => {
setActiveCategory(cat);
updateVisibleProducts(cat, products);
setActiveIndex(cat);
};
const updateVisibleProducts = (category, products) => {
let updatedProducts = [];
switch (category) {
case 0:
updatedProducts = products;
setVisibleProducts(updatedProducts);
break;
default:
updatedProducts = products.filter(
(product) => product.categories.indexOf(category) >= 0
);
setVisibleProducts(updatedProducts);
}
};
let currentLocation = window.location.href.split("/");
if (!loading) {
return (
<section className="htc__product__area shop__page mb--60 mt--130 bg__white">
<div className={currentLocation[3] == "" ? `container` : ""}>
<div className="htc__product__container">
<CategoryFilter
activeIndex={activeIndex}
categories={categories}
getCatId={getCatId}
/>
<div
className="product__list another-product-style"
style={{ height: "auto" }}
>
{visibleProducts
.slice(0, maxProducts ? maxProducts : products.length)
.map((prod, id) => {
return (
<ProductItem
wpApi={wpApi}
key={id}
onQuickViewed={onQuickViewed}
addedToCart={addedToCart}
product={prod}
/>
);
})}
</div>
</div>
</div>
</section>
);
} else {
return <Spinner />;
}
};
const mapStateToProps = ({ products, loading, activeCategory, categories }) => {
return {
products,
activeCategory,
categories,
loading,
};
};
const mapDispatchToProps = {
addedToCart,
categoriesLoaded,
onQuickViewed,
};
export default withWpApiService()(
connect(mapStateToProps, mapDispatchToProps)(Products)
);
and if you need, here is my CategoryFilter component ->
import React from 'react'
const CategoryFilter = ({categories, getCatId, activeIndex}) => {
return (
<div className="row mb--60">
<div className="col-md-12">
<div className="filter__menu__container">
<div className="product__menu">
{categories.map((cat) => {
return (
<button key={cat.id}
className={activeIndex === cat.id? 'is-checked' : null}
onClick={() => getCatId(cat.id)}
data-filter=".cat--4"
>
{cat.name}
</button>
);
})}
</div>
</div>
</div>
</div>
)
}
export default CategoryFilter

React Router changing URL, but component not rendering

I have been trying to learn React over the past couple of weeks and started working on a site which displays art works.
I would like for the user to be able to click on one of the images displayed and for a new component to be loaded with information about the work.
I have the implementation below of the gallery view, but when I click on an image the URL changes, but the WorkPage component never loads.
Would anyone be able to spot what I am doing wrong? The links and images are generated in the renderItems() function.
import React, { Component } from "react";
import Masonry from 'react-masonry-css';
import WorkPage from "./WorkPage"
import axios from "axios";
import { Link, Route, Switch, useRouteMatch, useParams } from "react-router-dom";
import { BrowserRouter as Router } from "react-router-dom";
class Works extends Component {
constructor(props) {
super(props);
this.state = {
viewPaintings: true,
workList: []
};
axios
.get("http://localhost:8000/api/works/")
.then(res => this.setState({ workList: res.data }))
.catch(err => console.log(err))
};
displayPaintings = status => {
if (status) {
return this.setState({ viewPaintings: true })
}
return this.setState({ viewPaintings: false })
};
renderTabList = () => {
return (
<div>
<ul className="tab-list-buttons">
<li onClick={() => this.displayPaintings(true)}
className={this.state.viewPaintings ? "active" : "disabled"}
key="button1"
>
Paintings
</li>
<li onClick={() => this.displayPaintings(false)}
className={this.state.viewPaintings ? "disabled" : "active"}
key="button2"
>
Works on Paper
</li>
</ul>
</div>
);
};
renderItems = () => {
const { viewPaintings } = this.state;
const newItems = viewPaintings
? this.state.workList.filter(item => item.type === 1)
: this.state.workList.filter(item => item.type === 0);
const breakpointColumnsObj = {
default: 4,
1100: 3,
700: 2,
500: 1
};
const items = newItems.map(item => (
<div key = {item.slug}>
<Link to={`${item.slug}`}>
<img src={item.image} alt={item.name} width="300"/>
</Link>
<Switch>
<Route path=":item.slug" component={WorkPage} />
</Switch>
</div>
));
return (
<Masonry
breakpointCols={breakpointColumnsObj}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{items}
</Masonry>
);
}
render() {
return (
<Router>
<div>
{ this.renderTabList() }
{ this.renderItems() }
</div>
</Router>
)
};
}
export default Works;

Material-UI Checkbox not work with Redux store

The source can be accessed in this repo
I used Redux store to update the checkbox's check flag, and I can see that the state is perfectly changing, but things does not get applied to React components.
I think everything is fine, but checkbox is not updated when I change the state coupled with check.
Redux store used is located in src/redux/modules/menu.js, and checkbox-related action creator is checkMenuNameList function.
Also, checkbox code can be found in src/containers/MenuEditContainer.js.
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as actions from '../redux/modules/menu'
import PageWrapper from '../components/base/PageWrapper'
import MenuEditWrapper from '../components/common/templates/MenuEditWrapper'
import MenuEditContent from '../components/common/content/MenuEditContent'
const MenuEditContainer = (props) => {
const handleInputChange = name => event => {
props.actions.changeInput({
key: name,
value: event.target.value
})
props.actions.generateMenuList(event.target.value)
}
const handleMenuNameCheckbox = index => event => {
props.actions.checkMenuNameList(index)
}
return (
<>
<PageWrapper>
<MenuEditWrapper>
<MenuEditContent
menuName={props.menuName}
menuPrice={props.menuPrice}
menuNameList={props.menuNameList}
handleInputChange={handleInputChange}
handleMenuNameCheckbox={handleMenuNameCheckbox}
/>
</MenuEditWrapper>
</PageWrapper>
</>
)
}
const mapStateToProps = ({ menu }) => ({
menuId: menu.menuId,
menuName: menu.menuName,
menuPrice: menu.menuPrice,
menuNameList: menu.menuNameList,
menuNameListChosen: menu.menuNameListChosen,
})
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actions, dispatch)
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(MenuEditContainer)
import React from 'react'
import { withRouter } from 'react-router-dom'
import Paper from '#material-ui/core/Paper'
import Typography from '#material-ui/core/Typography'
import TextField from '#material-ui/core/TextField'
import FormGroup from '#material-ui/core/FormGroup'
import FormControlLabel from '#material-ui/core/FormControlLabel'
import Checkbox from '#material-ui/core/Checkbox'
import PageTitle from '../../typography/PageTitle'
import AddButtonSet from '../../button/AddButtonSet'
import EditButtonSet from '../../button/EditButtonSet'
import { makeStyles } from '#material-ui/core/styles'
const useStyles = makeStyles(theme => ({
root: {
padding: theme.spacing(3, 2)
},
title: {
fontWeight: 200,
marginBottom: '1.5rem'
},
textField: {
marginLeft: theme.spacing(0.5),
marginRight: theme.spacing(1),
width: 200,
display: 'block'
},
}))
const MenuEditContent = (props) => {
const classes = useStyles()
const { match, location, history } = props
return (
<Paper className={classes.root}>
<PageTitle>
{
(location.pathname === '/menu/edit/new')
? (`새로운 메뉴 등록`)
// 기존 메뉴 수정시
: (match.params.menuId)
? (`메뉴 수정`)
: (``)
}
</PageTitle>
<TextField
id="menuName"
label="메뉴 이름"
type="search"
className={classes.textField}
margin="normal"
variant="outlined"
autoComplete="off"
value={props.menuName}
onChange={props.handleInputChange('menuName')}
/>
<TextField
id="menuPrice"
label="가격"
type="search"
className={classes.textField}
margin="normal"
variant="outlined"
autoComplete="off"
value={props.menuPrice}
onChange={props.handleInputChange('menuPrice')}
/>
{
(props.menuNameList.length > 0) && (
<FormGroup>
{
props.menuNameList.map((item, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={item.checked}
value={index}
onChange={props.handleMenuNameCheckbox(index)}
/>
}
label={item.name}
/>
))
}
</FormGroup>
)
}
{
(location.pathname === '/menu/edit/new')
? (
<AddButtonSet
onClickCreate={() => alert('create button!')}
onClickCancel={() => history.push('/menu')}
/>
)
: (match.params.menuId)
? (
<EditButtonSet
onClickUpdate={() => alert('update button!')}
onClickDelete={() => alert('delete button!')}
onClickCancel={() => history.push('/menu')}
/>
)
: (<></>)
}
</Paper>
)
}
export default withRouter(MenuEditContent)
Reason why my checkbox did not work was because I re-used the previous state incorrectly.
// redux/modules/menu.js
const updateMenuNameList = createAction(UPDATE_MENU_NAME_LIST, payload => ({ menuNameList: payload.menuNameList }))
export const checkMenuNameList = (index) => (dispatch, getState) => {
const { menu: { menuNameList } } = getState()
const previousStatus = menuNameList[index].checked
menuNameList[index].checked = !previousStatus
dispatch(onCheckMenuNameList({ updatedMenuNameList: menuNameList }))
}
In above example, I fetched previous state from getState(), and extract menuNameList from it. menuNameList's structure looks like below:
[
{
name: String,
checked: Boolean
}
]
Each checkbox uses this array to display name and determine whether it is checked or not. When I click any checkbox, handler will change the clicked checkbox's checked value.
The problem arise from here: I accidentally re-used objects from the previous menuNameList, and updated it only with certain checked value changed. This is not a correct approach, because even if an inside property is changed, Redux or React has no idea what have changed! React and Redux figure out state changes with shallow comparison of object. So, even if store changed, React does not render the view, and changes does not get applied to the view!
To avoid this problem, we should make a new object for menuNameList. Maybe it is recommended to use library like Immutable.js.
// redux/modules/menu.js
export const checkMenuNameList = (idx) => (dispatch, getState) => {
const { menu: { menuNameList } } = getState()
// deep copy
const newMenuNameList = menuNameList.map((item, index) => {
if (index !== idx) {
const newMenu = {
name: item.name,
checked: item.checked
}
return newMenu
} else {
const newMenu = {
name: item.name,
checked: !item.checked
}
return newMenu
}
})
dispatch(onCheckMenuNameList({ updatedMenuNameList: newMenuNameList }))
}

Categories

Resources