I am trying to implement a simple e-commerce application where I have a home component and a cart component whenever I call handle increment or handle decrement, quantity variable is increased or decreased twice in both cart array and items array.so {item.quantity} changes 0,2,4 and so on and this is happening on both cart page and home page. I feel like this has something to do with not using spread operator properly while updating an object in an array.
please help me understand why is this happening and how to resolve it.
Here is my code
shop.jsx
import React, { Component } from 'react';
import Navbar from './nav';
import Cart from './cart';
import Home from './home';
import { Switch, Route } from 'react-router-dom';
class Shop extends Component {
state = {
items: [],
cart: []
}
componentDidMount() {
let items = [...this.state.items];
items.push({ id: 1, name: "product1", price: 100, quantity: 0 });
items.push({ id: 2, name: "product2", price: 200, quantity: 0 });
this.setState({ items });
}
handleIncrement = (item) => {
console.log('handle increment called');
let items = [...this.state.items];
let cart = [...this.state.cart];
let productIndex = items.indexOf(item);
let cartIndex = cart.indexOf(item);
items[productIndex].quantity += 1;
if (cartIndex === -1) {
item.quantity += 1;
cart.push(item);
}
else {
cart[cartIndex].quantity += 1;
}
this.setState({ cart, items });
}
handleDecrement = (item) => {
console.log('handle decrement called');
let items = [...this.state.items];
let cart = [...this.state.cart];
let productIndex = items.indexOf(item);
let cartIndex = cart.indexOf(item);
items[productIndex].quantity -= 1;
cart[cartIndex].quantity -= 1;
this.setState({ cart, items });
}
render() {
return (
<div>
<Navbar />
<Switch>
<Route path="/home" render={(props) => <Home
items={this.state.items}
handleAdd={this.handleAdd}
handleIncrement={this.handleIncrement}
handleDecrement={this.handleDecrement}
{...props} />} />
<Route path="/cart" render={(props) => <Cart
cart={this.state.cart}
handleIncrement={this.handleIncrement}
handleDecrement={this.handleDecrement}
{...props} />} />
</Switch>
</div>
);
}
}
export default Shop;
Home.jsx
import React from 'react';
const Home = (props) => {
return (
<div>
Home
<ul>
{props.items.map((item) => {
return (
<li key={item.id}>
{item.name} <br />
{item.price} <br />
{
item.quantity === 0 ?
<button onClick={() => props.handleIncrement(item)}>add</button> :
<div>
<button onClick={() => props.handleDecrement(item)}>-</button>
<button>{item.quantity}</button>
<button onClick={() => props.handleIncrement(item)}>+</button>
</div>
}
</li>
)
})
}
</ul>
</div>
);
}
export default Home;
cart.jsx
import React from 'react';
const Cart = (props) => {
return (
<div>
cart
<ul>
{props.cart.map((item) => {
return (
<li key={item.id}>
{item.name} <br />
{item.price} <br />
{item.quantity} <br />
{item.price * item.quantity}
</li>
)
})}
</ul>
</div>
);
}
export default Cart;
nav.jsx
import React from 'react';
import { Link } from 'react-router-dom';
const Navbar = () => {
return (
<div>
Navbar
<Link to="/home">Home</Link>
<Link to="/cart">Cart</Link>
</div>
);
}
export default Navbar;
Remove items[productIndex].quantity += 1; from handleIncrement and items[productIndex].quantity -= 1; from handleDecrement method.
This will work.
Related
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}/>
Description of problem:
Changing the id (numbers only) of this url via the link tag does not update the page (but does change the url in the adress bar). Hitting refresh afterward will show the updated page.
http://localhost:8080/video/id/7564
Right clicking to open the link in a new tab, or changing the link path to a completely different page works as expected.
My app.js file
import React from 'react'
import { Router, Route, Switch } from 'react-router-dom'
import RenderHomepage from '../components/homePage/RenderHomepage'
import RenderChannelPage from '../components/channelPage/RenderChannelPage'
import RenderVideoPage from '../components/videoPage/RenderVideoPage'
import RenderSearchPage from '../components/searchPage/RenderSearchPage'
import PageNotFound from '../components/PageNotFound'
import history from '../history'
const App = () => {
return (
<div>
<Router history={history}>
<Switch>
<Route path="/" exact component={RenderHomepage} />
<Route path="/channel" component={RenderChannelPage} />
<Route path="/video/id" component={RenderVideoPage} />
<Route path="/search" component={RenderSearchPage} />
<Route path="/404" exact component={PageNotFound} />
<Route component={PageNotFound} />
</Switch>
</Router>
</div>
)
}
export default App
Link tag in UpNextVideos component:
import React from 'react'
import { Link } from 'react-router-dom'
...
<Link to={{pathname: vid.id}}>
<h3 className={`${p}-sidebar-grid-video-title`}>{capitalizeFirstLetter(vid.tags)}</h3>
</Link>
...
How the components in question are nested:
<RenderVideoPage>
<VideoPage>
<UpNextVideos>
RenderVideoPage component:
import React from 'react'
import VideoPage from './VideoPage'
import Header from '../Header'
import HeaderMobile from '../HeaderMobile'
import FooterMobile from '../FooterMobile'
import ActivityFeed from '../ActivityFeed'
const RenderVideoPage = () => {
return (
<div className="videoPage-body">
<HeaderMobile />
<Header />
<ActivityFeed page={'home'} />
<VideoPage />
<FooterMobile page={'video'} />
</div>
)
}
export default RenderVideoPage
VideoPage component:
import React, { useEffect, useState } from 'react'
import axios from 'axios'
import history from '../../history'
import handleMediaQueries from './containers/handleMediaQueries'
import setDislikes from './containers/setDislikes'
import NewSubscribers from './NewSubscribers'
import CommentSection from './CommentSection'
import UpNextVideos from './UpNextVideos'
import DescriptionBox from './DescriptionBox'
import VideoNotFound from './VideoNotFound'
import { fetchVideoFromID, fetchPictureFromID } from '../../containers/api'
import { thumbsUp, thumbsDown } from '../svgs'
import {
abbreviateNumber,
capitalizeFirstLetter,
randomDate } from '../../containers/helperFunctions'
const VideoPage = () => {
const [p, setPrefix] = useState("videoPage")
const [state, setState] = useState({
loading: true,
error: false
})
useEffect(() => {
if (state.loading) extractDataFromUrl()
else handleMediaQueries()
}, [state.loading])
const fetchVideo = async (id, picAuthorID) => {
let response = await fetchVideoFromID(id)
if (!response) setState(prevState => ({...prevState, error: true}))
else mapVideoResponseToHTML(response.data.hits, picAuthorID)
}
const mapVideoResponseToHTML = (response, picAuthorID) => {
let responseAsHtml = response.map(vid => {
return {
video:
<div className={`${p}-video-wrapper posRelative`} key={vid.id}>
<a className={`${p}-pixabay-src`} href={vid.pageURL}>?</a>
<video
poster="https://i.imgur.com/Us5ckqm.jpg"
className={`${p}-video clickable`}
src={vid.videos.large.url}
controls autoPlay>
</video>
<div className={`${p}-video-info-wrapper`}>
<div className={`${p}-video-title-box`}>
<h1 className={`${p}-video-title`}>{capitalizeFirstLetter(vid.tags)}</h1>
<span className={`${p}-video-views`}>{abbreviateNumber(Number(vid.downloads).toLocaleString())} views</span>
<span className={`${p}-video-date`}>{randomDate()}</span>
</div>
<div className={`${p}-video-options`}>
<div className="thumbs">
<div className={`${p}-video-options-thumbsUp`}>{thumbsUp(20)}
<span className={`${p}-video-options-thumbsUp-text`}>{abbreviateNumber(vid.likes)}</span>
</div>
<div className={`${p}-video-options-thumbsDown`}>{thumbsDown(20)}
<span className={`${p}-video-options-thumbsDown-text`}>{setDislikes(vid.likes)}</span>
</div>
<div className={`${p}-video-options-likebar`}></div>
</div>
<span className={`${p}-video-options-share`}>Share</span>
<span className={`${p}-video-options-save`}>Save</span>
<span className={`${p}-video-options-ellipses`}>...</span>
</div>
</div>
</div>,
authorFollowers: vid.views,
vidAuthorID: vid.id,
author: picAuthorID ? 'Loading' : vid.user,
authorAvatar: picAuthorID ? null : vid.userImageURL,
views: vid.downloads
}
})
responseAsHtml = responseAsHtml[0]
setState(prevState => ({...prevState, ...responseAsHtml, loading: false}))
if (picAuthorID) fetchAuthorAvatar(picAuthorID)
}
const extractDataFromUrl = () => {
const currentURL = window.location.href
const urlAsArray = currentURL.split('/')
const urlID = urlAsArray[5].split('-')
const videoID = urlID[0]
const picAuthorID = urlID[1]
// Author avatars are random except on the home page.
// if url isnt from homepage, then use videoID
// if url is from homepage, send that avatarID
if (urlID.includes('000')) {
fetchVideo(videoID)
} else {
setState(prevState => ({...prevState, picAuthorID: picAuthorID}))
fetchVideo(videoID, picAuthorID)
}
}
const fetchAuthorAvatar = async (id) => {
const response = await fetchPictureFromID(id)
const authorName = response.data.hits[0].user
const authorAvatar = response.data.hits[0].previewURL
setState(prevState => ({
...prevState,
authorAvatar: authorAvatar,
author: capitalizeFirstLetter(authorName)
}))
}
return (
<div>
{ state.error ? <VideoNotFound /> : null}
{ state.loading === true ? null
:
<div className={`${p}-page-wrapper`}>
<main className={`${p}-main`}>
{state.video}
<DescriptionBox props={state} />
<div className={`${p}-suggested-videos-mobile`}></div>
<div className={`${p}-new-subscribers-wrapper`}>
<h2 className={`${p}-new-subscribers-text`}>{`New Subscribers to ${state.author}`}</h2>
<NewSubscribers />
</div>
<div className={`${p}-comment-section`}>
<CommentSection views={state.views}/>
</div>
</main>
<aside className={`${p}-sidebar`}>
<UpNextVideos />
</aside>
</div>
}
</div>
)
}
export default VideoPage
UpNextVideos component:
import React, { useEffect, useState, useRef, useCallback } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import { videoQuery } from '../../words'
import { fetchVideos } from '../../containers/api'
import {
capitalizeFirstLetter,
uuid,
getRandom,
abbreviateNumber
} from '../../containers/helperFunctions'
const UpNextVideos = () => {
const [p, setPrefix] = useState("videoPage")
const [nextVideos, setNextVideos] = useState([])
useEffect(() => {
fetchUpNextVideos(15, getRandom(videoQuery))
}, [])
// INFINITE SCROLL
const observer = useRef()
const lastUpNextVideo = useCallback(lastVideoNode => {
// Re-hookup observer to last post, to include fetch data callback
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver(entries => {
const lastVideo = entries[0]
if (lastVideo.isIntersecting && window.innerWidth <= 1000) {
document.querySelector('.videoPage-show-more-button').classList.add('show')
}
else if (lastVideo.isIntersecting && window.innerWidth > 1000) {
document.querySelector('.videoPage-show-more-button').classList.remove('show')
fetchUpNextVideos(20, getRandom(videoQuery))
}
})
if (lastVideoNode) observer.current.observe(lastVideoNode)
})
const fetchUpNextVideos = async (amount, query) => {
let response = await fetchVideos(amount, ...Array(2), query)
response = response.data.hits
const responseAsHtml = response.map((vid, index) => {
return (
<div className={`${p}-sidebar-grid-video-wrapper`} key={uuid()} ref={response.length === index + 1 ? lastUpNextVideo : null}>
<div className={`${p}-sidebar-grid-video`}>
<a href={`/video/id/${vid.id}-000`}>
<video
className={`${p}-upnext-video`}
onMouseOver={event => event.target.play()}
onMouseOut={event => event.target.pause()}
src={`${vid.videos.tiny.url}#t=1`}
muted >
</video>
</a>
</div>
<a href={`/video/id/${vid.id}`}>
<h3 className={`${p}-sidebar-grid-video-title`}>{capitalizeFirstLetter(vid.tags)}</h3>
</a>
<a href={`/channel/000${vid.id}`}>
<p className={`${p}-sidebar-grid-video-author`}>{vid.user}</p>
</a>
<p className={`${p}-sidebar-grid-video-views-text`}>{abbreviateNumber(vid.downloads)} views</p>
</div>
)
})
setNextVideos(prevState => ([...prevState, ...responseAsHtml]))
}
return (
<div>
<div className={`${p}-sidebar-text-top`}>
<span className={`${p}-sidebar-text-upnext`}>Up next</span>
<span className={`${p}-sidebar-text-autoplay`}>Autoplay</span>
</div>
<div className={`${p}-sidebar-grid-wrapper`}>
{nextVideos}
</div>
<button
className={`${p}-show-more-button`}
onMouseDown={() => fetchUpNextVideos(15, getRandom(videoQuery))}>
Show More
</button>
</div>
)
}
export default UpNextVideos
What I've tried:
Wrapping the <Link> tag with <Router history={history} />
Wrapping the <Link> tag with <BrowserRouter>
Wrapping the export statement withRouter(UpNextVideos)
Using a plain string instead of an object, as described in react-router-docs
Ok, I believe this issue lies in your VideoPage component.
useEffect(() => {
if (state.loading) extractDataFromUrl()
else handleMediaQueries()
}, [state.loading]);
You only ever have state.loading true once, when the component mounts. This only processes your URL once, so when the URL changes this component isn't aware of it.
This is your route currently
<Route path="/video/id" component={RenderVideoPage} />
now assuming your URLs are shaped "/video/id/" then you can define your route to have a parameter
<Route path="/video/id/:videoId" component={RenderVideoPage} />
If you wrap this component with react-router-dom's withRouter HOC you can easily get the id path param and add it to an effect to recompute all the video data.
export default withRouter(VideoPage)
withRouter injects the location, match, and history props from the closest Route ancestor. Here's an example of getting the id param and triggering an effect when its value updates.
const VideoPage = ({ match }) => {
const { params } = match;
useEffect(() => { /* do something with new id */ }, [params.videoId]);
}
I'm making a simple shopping cart app using Redux. Right now every time I change page, actions are automatically get called for three times which is equal to the number of items. If I go to Cart page, removeItems action gets called three times so there's no way I can add items to cart so far. It might be a problem about router, but I can't spot the problem. Could anyone explain me what is the problem?
Home.js
import React from 'react';
import { connect } from 'react-redux';
import { addToCart } from '../actions';
class Home extends React.Component {
handleClick = id => {
this.props.addToCart(id)
}
renderList = () => {
return this.props.cart.slice(0, 3).map(item => {
return (
<div className="card" key={item.id} style={{width: "200px", float: "left", marginRight: "20px"}}>
<div className="card-image">
<img src={item.imageUrl} alt={item.name} />
<span className="card-title">{item.name}</span>
<span to="/"
className="btn-floating halfway-fab waves-effect waves-light red"
onClick={this.handleClick(item.id)}
>
<i className="material-icons">add</i>
</span>
</div>
<div className="card-content">
<p>{item.desc}</p>
<p><b>${item.price}</b></p>
</div>
</div>
)
})
}
render() {
console.log(this.props.cart)
return (
<div className="container">
<h3>Home</h3>
<div className="box">
{this.renderList()}
</div>
</div>
)
}
}
const mapStateToProps = state => {
return { cart: state.cart.items }
}
const mapStateToDispatch = dispatch => {
return {
addToCart: (id) => { dispatch(addToCart(id)) }
}
}
export default connect(mapStateToProps, mapStateToDispatch)(Home);
Cart.js
import React from 'react';
import { connect } from 'react-redux';
import { removeItem } from '../actions';
class Cart extends React.Component {
handleClick = (id) => {
this.props.removeItem(id);
}
renderList = () => {
if (this.props.addedItems.length !== 0) {
return this.props.addedItems.map(item => {
return (
<li className="collection-item avatar" key={item.id}>
<div className="item-img">
<img src={item.imageUrl} alt={item.name} style={{width: "120px"}} />
</div>
<div className="item-desc">
<span className="title">{item.name}</span>
<p>{item.content}</p>
<p><b>${item.price}</b></p>
</div>
<button
className="waves-effect waves-light btn pink remove"
onClick={this.handleClick(item.id)}
>Remove</button>
</li>
)
})
}
else {
return <p>Nothing is in cart.</p>
}
}
render() {
return (
<div className="container">
<div className="cart">
<ul className="collection">
{this.renderList()}
</ul>
</div>
</div>
)
}
}
const mapStateToProps = state => {
return { addedItems: state.cart.addedItems }
}
const mapDispatchToProps = dispatch => {
return {
removeItem: (id) => {dispatch(removeItem(id))}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Cart);
Header.js
import React from 'react';
import { Link } from 'react-router-dom';
const Header = () => {
return (
<nav className="nav-wrapper">
<div className="container">
<Link to="/" className="brand-logo">Shopping</Link>
<ul className="right">
<li><Link to="/">Shop</Link></li>
<li><Link to="/cart">Cart</Link></li>
<li><Link to="/cart"><i className="material-icons">shopping_cart</i></Link></li>
</ul>
</div>
</nav>
)
}
export default Header;
Reducers
import data from '../data.json';
import { ADD_TO_CART, REMOVE_FROM_CART } from "../actions/types";
const INITIAL_DATA = {
items: data,
addedItems: [],
total: 0
}
const cartReducer = (state = INITIAL_DATA, action) => {
switch(action.type) {
case ADD_TO_CART:
let addedItem = state.items.find(item => item.id === action.id);
let existedItem = state.addedItems.find(item => action.id ===item.id);
if (existedItem) {
addedItem.quantity += 1;
return {
...state,
total: state.total + addedItem.price
}
}
else {
addedItem.quantity = 1;
let newTotal = state.total + addedItem.price;
return {
...state,
addedItems: [...state.addedItems, addedItem],
total: newTotal
}
}
case REMOVE_FROM_CART:
let itemToRemove = state.addedItems.find(item => action.id === item.id);
let newItems = state.addedItems.filter(item => action.id !== item.id);
let newTotal = state.total - itemToRemove.price;
return {
...state,
addedItems: newItems,
total: newTotal
}
default:
return state;
}
}
export default cartReducer;
Actions
import { ADD_TO_CART, REMOVE_FROM_CART } from "./types";
export const addToCart = (id) => {
return {
type: ADD_TO_CART,
id
}
}
export const removeItem = (id) => {
return {
type: REMOVE_FROM_CART,
id
}
}
App.js
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Header from './Header';
import Home from './Home';
import Cart from './Cart';
const App = () => {
return (
<BrowserRouter>
<div className="app">
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/cart" component={Cart} />
</Switch>
</div>
</BrowserRouter>
)
}
export default App;
Seems like you're actually calling your click handlers whenever you render your components, instead of just passing the handler function, so that's why actions are being triggered multiple times.
For instance, in your Home.js component change the code below from:
<span to="/" className="btn-floating halfway-fab waves-effect waves-light red" onClick={this.handleClick(item.id)} >
to:
<span to="/" className="btn-floating halfway-fab waves-effect waves-light red" onClick={() => { this.handleClick(item.id); }} >
And the same thing on Cart.js, change from:
<button className="waves-effect waves-light btn pink remove" onClick={this.handleClick(item.id)}>Remove</button>
to:
<button className="waves-effect waves-light btn pink remove" onClick={() => {this.handleClick(item.id); }}>Remove</button>
This might be a double of some question, but I couldn't find the answer to the specific question that I have. I have the following code:
import React, { Component } from 'react'
class FAQContent extends Component {
constructor(props) {
super(props);
this.state = {
opened: false,
};
this.toggleBox = this.toggleBox.bind(this);
}
toggleBox() {
const { opened } = this.state;
this.setState({
opened: !opened,
});
}
render() {
return (
<div>
<div className="question">
<div className="question-title" onClick={this.toggleBox}>
Title 1
</div>
{this.state.opened && (
<div class="answer">
Content 1
</div>
)}
</div>
<div className="question">
<div className="question-title" onClick={this.toggleBox}>
Title 2
</div>
{this.state.opened && (
<div class="answer">
Content 2
</div>
)}
</div>
</div>
)
}
}
export default FAQContent
This renders 2 question titles. However, when I click on any of the questions, the state change is triggered for all the questions. What is the most efficient way of showing the specific answer of the question without showing the rest of the components?
import React, { Component } from "react";
import { render } from "react-dom";
import { Link, BrowserRouter, Route } from "react-router-dom";
class App extends Component {
state = {
openedPost: "",
posts: [
{ question: "Question 1", id: 0, user: "lenny" },
{ question: "Question 2", id: 1, user: "benny" },
{ question: "Question 3", id: 2, user: "jenny" }
]
};
showPost = id => {
this.setState({ openedPost: id });
};
render() {
return (
<div>
<BrowserRouter>
<div>
<Route
path="/"
render={() => (
<Posts showPost={this.showPost} posts={this.state.posts} />
)}
/>
<Route
exact
path={`/posts/${this.state.openedPost}`}
render={() => (
<SinglePost
openedPost={this.state.openedPost}
showPost={this.showPost}
posts={this.state.posts}
/>
)}
/>
</div>
</BrowserRouter>
</div>
);
}
}
class Posts extends Component {
onClick = id => {
this.props.showPost(id);
};
render() {
const { posts, showPost } = this.props;
return (
<div>
{posts.map(item => (
<div onClick={() => this.onClick(item.id)}>
<Link to={`/posts/${item.id}`}>{item.question} </Link>{" "}
</div>
))}
</div>
);
}
}
class SinglePost extends Component {
render() {
const { posts, openedPost } = this.props;
const filtered = posts.filter(item => item.id === openedPost);
return (
<div>
{filtered.map(item => (
<div>
{" "}
QUESTION:{item.question} ID:{item.id}{" "}
</div>
))}
</div>
);
}
}
render(<App />, document.getElementById("root"));
Example
You are using a same state to control different parts. How about you make a new question component and let it to manage its own state and just use the question component in the FAQContent component.
Question component:
export default class Question extends Component {
state = { opened: false };
toggleBox = () => this.setState(state => ({ opened: !state.opened }));
render() {
return (
<div className="question">
<div className="question-title" onClick={this.toggleBox}>
{this.props.title}
</div>
{this.state.opened && (
<div class="answer">
{this.props.content}
</div>
)}
</div>
);
}
}
FAQContent Component:
const FAQContent = () => (
<div>
<Question title="title 1" content="content 1" />
<Question title="title 2" content="content 2" />
</div>
);
export default FAQContent;
My project file structure
App.jsx
import React, { Component } from 'react';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import { Router, Route ,browserHistory} from 'react-router';
import Header from './components/header';
import PreLoginHeader from './components/pre_login_header';
import BrandManagerForm from './components/brand_manager_form';
import Home from './pages/home';
import Internships from './pages/internships';
import PostNewInternship from './pages/post_new_internship';
import Applications from './pages/applications';
import Support from './pages/support';
require('./util/config-reflux')();
class App extends Component {
componentDidMount(){
}
render() {
const history = new createBrowserHistory();
return (
<div style={{height:'100%',width:'100%'}}>
{
this.checkLoggedIn() ? <Header history={history} /> : <PreLoginHeader />
}
<Router history={history}>
<Route path='/' component={Home} />
<Route path='/internships' component={Internships} />
<Route path='/register' component={BrandManagerForm} />
<Route path='/register/complete' component={BrandManagerForm} />
<Route path='/post-new-internship' component={PostNewInternship} />
<Route path='/applications' component={Applications} />
<Route path='/messages' component={Internships} />
<Route path='/messages/:id' component={Support} />
<Route path='/messages/:id/edit' component={PostNewInternship} />
{/* IT'S THE SAME VIEW AS Internships.NOT TO BE CONFUSED WITH NAME*/}
</Router>
</div>
)
}
checkLoggedIn = () => {
if (localStorage.getItem('loggedIn'))
return true
return false
}
}
export default App;
config-reflux.jsx
var Reflux = require('reflux');
var RefluxPromise = require('reflux-promise');
module.exports = function() {
// Uses the user agent's Promise implementation
Reflux.use(RefluxPromise(window.Promise)); // eslint-disable-line new-cap
};
Support.jsx
/* eslint-disable */
import React,{Component} from 'react'
import '../assets/styles/support.css'
import request from 'request'
import Breadcrumb from 'react-breadcrumb'
import moment from 'moment'
import Loader from '../components/loader'
import supportAction from '../actions/supportAction'
class Support extends Component {
constructor(props){
super(props)
this.base_url = process.env.REACT_APP_API_URL;
this.state = {
message : '',
messages:[]
}
}
componentDidMount(){
if (!this.checkLoggedIn()){
window.location = '/'
}
const that = this;
var buffer_id = this.props.params.id;
this.setState({buffer_id:buffer_id},function(){
this.fetchData()
})
var data = null
request.get({
url:`${this.base_url}/internshipbuffer/${buffer_id}`,
headers:{
'Authorization':`Bearer ${localStorage.getItem('auth-token')}`
}
},function(err,resp,body){
data = JSON.parse(body)
data.benefits = data.benefits.split('\n')
that.setState({data:data})
})
}
updateData = (result) => {
console.log(result)
}
fetchData = () => {
var params = {
'internshipbuffer':this.state.buffer_id
}
supportAction.getMessage(params).then(this.updateData)
}
getMessage = (next) => {
const that = this;
var url = `${this.base_url}/internshipbuffer/getChat?internshipbuffer=${this.state.buffer_id}`;
if (next){
url = `${this.base_url}/internshipbuffer/getChat?internshipbuffer=${this.state.buffer_id}&sort={"createdAt":-1}`
}
request.get({
url:url,
headers : {
"Authorization":`Bearer ${localStorage.getItem('auth-token')}`
}
},function(err,resp,body){
var body = JSON.parse(body)
console.log(body);
if (body){
var messages = that.state.messages
if (next){
messages = messages.concat(body.data.reverse())
}
else{
messages = body.data.reverse()
}
that.setState({messages:messages,messages_next:body.next})
}
})
}
render(){
return (
<div className="support-main">
{this.state.data ? this.renderContent() : this.renderLoading()}
</div>
)
}
checkLoggedIn = () => {
if (localStorage.getItem('loggedIn') ){
return true ;
}
return false;
}
renderLoading = () => {
return (
<Loader />
)
}
renderContent = () => {
const that = this;
var data = this.state.data;
return (
<div className="support-main-container">
<Breadcrumb
path={
[
{
path: '/messages',
label: 'Messages '
},
{
path: `/pending/${this.props.params.id}`,
label: ` ${data.title}`
}
]
}
separatorChar={' > '}
/>
<div className="card">
<div className="img-container">
<img alt={null} src={data.asseturl} id="support-brand-logo" width="50"/>
</div>
<ul className="card-brand-info-container">
<li className="bold">{localStorage.getItem('brand_name')}</li>
<li>Brand manager : {localStorage.getItem('brandmanager_name')}</li>
</ul>
<button onClick={this.editInternshipAction} className='support-edit-btn'>EDIT</button>
<div className="support-main-info">
<div> <span className="bold">Title</span> : {data.title}</div>
<div><span className="bold"> Description</span> : {data.description}</div>
<div><span className="bold"> Positions</span> : {data.positions}</div>
<div><span className="bold">Intenship Period</span> : {data.period} </div>
<div>
<span className="bold"> Benefits </span>:
<ul className="benefits-list">
{data.benefits[0]}
</ul>
</div>
<div>
<span className="bold"> Skills & Requirements</span> :
<ul className="skills-list">
{data.requirements}
</ul>
</div>
<div>
<span className="bold"> City : </span>
<ul className="city-list">
{
data.citiesvalid.map(function(item,i){
if (i !== (data.citiesvalid.length)-1)
return <li key={i} >{item},</li>
else
return <li key={i} >{item}</li>
})
}
</ul>
</div>
</div>
</div>
<div className="messages-container">
{
this.state.messages ?
this.state.messages.map(function(obj,i){
var messenger;
obj.brandmanager ? messenger = localStorage.getItem('brand_name') : messenger = 'Support'
return (that.renderMessage(obj.message,messenger,i,obj.createdAt))
}) : null
}
</div>
<div className="message-input-container">
<input className="prefix-messenger" placeholder={`${localStorage.getItem('brand_name')} :`} />
<input className="main-message" value={this.state.message} onChange={this.setMessage} />
</div>
<button onClick={this.sendMessage} className="messages-send-btn"><i style={{marginRight:'5px'}} className="fa fa-paper-plane" aria-hidden="true"></i>Send</button>
</div>
)
}
editInternshipAction = () => {
this.props.router.push(this.props.location.pathname+'/edit')
}
renderMessage = (message,messenger,i,message_date) => {
var data = this.state.data;
return (
<div key={i} className="support-message-box">
<hr />
<div className="img-container-message">
<img alt={null} src={data.asseturl} id="support-brand-logo" width="50"/>
</div>
<div className="main">
<div>
<span style={{color:'#c67763'}} className={`bold ${messenger}`}>{messenger}</span>
<span> ({moment(new Date(message_date)).format('LT')}) : {message} </span>
</div>
</div>
</div>
)
}
sendMessage = () => {
const that = this;
if (this.state.message){
request.put({
url:`${this.base_url}/internshipbuffer/putChat?internshipbuffer=${this.props.params.id}&message=${this.state.message}`,
headers:{
"Authorization":`Bearer ${localStorage.getItem('auth-token')}`
}
},function(err,resp,body){
that.setState({message:''})
that.getMessage();
})
}
else{
this.setState({message_error:'error',error_message:'Required'})
}
that.scrollToBottom(document.getElementsByClassName('messages-container')[0])
}
scrollToBottom = (element) => {
console.log(element.scrollHeight)
element.scrollTop = element.scrollHeight;
}
setMessage = (e) => {
this.setState({message:e.target.value})
if (e.target.value === ''){
this.setState({message_error:'error',error_message:'Required'})
}
else{
this.setState({message_error:'',error_message:''})
}
}
}
export default Support
supportAction.jsx
import Reflux from 'reflux'
import supportResource from '../resources/supportResource'
var actions = Reflux.createActions({
'getMessage': {
children: ['completed', 'failed']
}
});
console.log(actions)
actions.getMessage.listenAndPromise(supportResource.getMessage);
export default actions;
I have used reflux-promise module in my react app.
I have defined an action to fetch resource which fetches the data from the server.
I have used listenAndPromise in my action .
I have configured my app to use promise in config-reflux.jsx file.
Then too its showing listenAndPromise is not a function.
Any help ? Thanks !!
I think the order of what you import is wrong. Try to import the config before using this function.
import './util/config-reflux'; // <-- make sure you import it before any component or action using it
import Support from './pages/support';
config-reflux.jsx:
var Reflux = require('reflux');
var RefluxPromise = require('reflux-promise');
// Uses the user agent's Promise implementation
Reflux.use(RefluxPromise(window.Promise)); // eslint-disable-line new-cap