I'm building a shopping cart app and I want to prevent an item from appearing on the cart page twice if there's a duplicate. I'm trying to check an array of items objects from the useState hook. If the array contains an object with the item name, I want to prevent it from being added twice, otherwise I want to push it into the array. But when I console log the array, it doesn't update right away. Also, after I hit the "addItemToCart" button on ProductsDetails.jsx twice, I get an error "TypeError: Cannot read property 'find' of undefined." Sorry if I'm not explaining it clearly. Here's my code. Any help would be appreciated.
App.js
function App() {
const [cartItems, setCartItems] = useState([]);
const [cartCount, setCartCount] = useState(0);
const [cartTotal, setCartTotal] = useState(0.00);
return (
<div className="App">
<BrowserRouter>
<NavigationBar count={cartCount} />
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/products" component={ProductsPage} />
<Route exact path="/about" component={AboutPage} />
<Route exact path="/profile" component={ProfilePage} />
<Route
exact
path="/cart"
render={() => {
return (
<CartPage
setCartCount={setCartCount}
cartItems={cartItems}
cartTotal={cartTotal}
cartCount={cartCount}
/>
);
}}
/>
<Route
exact
path="/:details"
render={() => (
<ProductDetails
cartItems={cartItems}
setCartItems={setCartItems}
setCartCount={setCartCount}
setCartTotal={setCartTotal}
/>
)}
/>
</Switch>
</BrowserRouter>
</div>
);
}
export default App;
ProductsDetails.jsx
import React from "react";
import "./ProductDetails.css";
import { useLocation } from "react-router-dom";
const ProductDetails = (props) => {
const location = useLocation();
const products = JSON.parse(localStorage.getItem("data"));
const productInfo = products.find(
(item) => item.name === location.state.name
);
const { name, image, price } = productInfo;
const cartCounter = (counter) => {
props.setCartCount((counter += 1));
};
const cartPriceCounter = (counter) => {
props.setCartTotal((counter += price));
};
const addItemToCart = () => {
const product = {
name,
price,
};
if(!props.cartItems.find(e => e.name === product.name)){
props.setCartItems([...props.cartItems, product]);
}
console.log(props.cartItems)
};
return (
<div className="details-container">
<div className="details-card">
<img className="details-img" src={image} alt={name} />
<div className="details-div">
<h3 className="details-title">{name}</h3>
<p className="details-price">${price}</p>
<button
className="add-to-cart"
onClick={() => {
props.setCartCount(cartCounter);
props.setCartTotal(cartPriceCounter);
props.setCartItems(addItemToCart);
}}
>
Add To Cart
</button>
</div>
</div>
</div>
);
};
export default ProductDetails;
The mutated state won't be reflected in the same render(i.e. in console.log), it would be updated when the next render occurs due to state change
See https://medium.com/ableneo/react-setstate-does-not-immediately-update-the-state-84dbd26f67d5
You could use setState with function argument which would have the updated value.
Related
I am trying to add the items to a cart page when a user clicks the add to cart button.
import React from "react";
import "bootstrap";
import { useParams } from "react-router-dom";
function ItemDetail(handleClick) {
const params = useParams();
let { productCode, vendor, value} = params;
let item = {productCode, vendor, value};
console.log(item);
return (
<>
<div>
<p>product id: {productCode}</p>
<p>price: {value}</p>
<p>vendor: {vendor}</p>
<button onClick={() => handleClick(item)}>Add to Cart</button>
</div>
</>
);
}
export default ItemDetail;
This is the cart page. Where I am to, render the item details from Item Details Page.
import React, { useState, useEffect } from "react";
const Cart = ({ cart, setCart, handleChange }) => {
const [price, setPrice] = useState(0);
const handleRemove = (id) => {
const arr = cart.filter((item) => item.id !== id);
setCart(arr);
handlePrice();
};
const handlePrice = () => {
let ans = 0;
cart.map((item) => (ans += item.amount * item.price));
setPrice(ans);
};
useEffect(() => {
handlePrice();
});
console.log(setCart);
return (
<article>
{cart.map((item) => (
<div className="cart_box" key={item.id}>
<div>
<button onClick={() => handleChange(item, 1)}>+</button>
<button>{item.amount}</button>
<button onClick={() => handleChange(item, -1)}>-</button>
</div>
<div>
<span>{item.price}</span>
<button onClick={() => handleRemove(item.id)}>Remove</button>
</div>
</div>
))}
<div className="total">
<span>Total Price of your Cart</span>
<span>R - {price}</span>
</div>
</article>
);
};
export default Cart;
This is my item description page. I have fetched the items using params, this is only way I found easier for me.
import React, { useState, useEffect } from "react";
import { Row, Col } from "react-bootstrap";
import StyledCard from "../components/Card";
const Discover = (props, params, handleClick) => {
const token = "not-the-actual-token";
const [result, setResult] = useState([]);
useEffect(() => {
fetch(
"https://api.flash-internal.flash-group.com/ecommerceManagement/1.0.0/api/product/",
{
method: "GET",
headers: { Authorization: `Bearer ${token}` },
}
)
.then((res) => res.json())
.then((json) => setResult(json));
}, []);
const cardStyle = {
listStyle: "none",
margin: 5,
paddingLeft: 0,
minWidth: 240,
};
return (
<>
<div className="latestdeals container my-5">
<h1>All Products</h1>
<Row className="hotcards">
<Col className="colcard">
{(result?.result || []).map((item) => (
<div key={item.productCode} style={cardStyle}>
<a href={`/itemDetail/${item.productCode}/${item.value}/${item.vendor}`}>
{" "}
<StyledCard
key={item.productCode}
name={item.vendor}
title={item.description}
price={item.value}
handleClick={handleClick}
item={item}
/>
</a>
</div>
))}
</Col>
</Row>
</div>
</>
);
};
export default Discover;
This is my App page
import "./index.scss";
import React, { useState } from "react";
import {
BrowserRouter as Router,
Route,
Routes,
useParams,
} from "react-router-dom";
import AllCategories from "./pages/all-catergories";
import Home from "./pages/home";
import Entertainment from "./pages/entertainment";
// import Cart from "./pages/_cart";
import Login from "./pages/login";
import Netflix from "./pages/netflix";
import Orders from "./pages/orders";
import SignUp from "./pages/sign-up";
// import Data2 from "./Data2";
import Products from "./pages/products";
// import Shop from "./components/Shop";
// import ProductDetail from "./pages/ProductDetail";
import Discover from "./pages/discover";
import ItemDetail from "./pages/itemDetail";
import Cart from "./pages/cart";
function App() {
const [show, setShow] = useState(true);
const [cart, setCart] = useState([]);
const handleClick = (item) => {
if (cart.indexOf(item) !== -1) return;
setCart([...cart, item]);
};
const handleChange = (item, d) => {
const ind = cart.indexOf(item);
const arr = cart;
arr[ind].amount += d;
if (arr[ind].amount === 0) arr[ind].amount = 1;
setCart([...arr]);
};
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="all-categories" exact element={<AllCategories />} />
{/* <Route path="cart" exact element={<Cart />} /> */}
<Route path="entertainment" exact element={<Entertainment />} />
<Route path="login" exact element={<Login />} />
<Route path="discover" exact element={<Discover />} />
<Route path="netflix" exact element={<Netflix />} />
<Route path="orders" exact element={<Orders />} />
<Route path="sign-up" exact element={<SignUp />} />
<Route path="products" element={<Products />} />
<Route path="/itemDetail/:productCode/:value/:vendor" element={<ItemDetail />} />
<Route path="/itemDetail/" element={<ItemDetail handleClick={handleClick} />} />
<Route path="/Cart/" exact element={<Cart cart={cart} setCart={setCart} handleChange={handleChange}/>} />
</Routes>
</Router>
);
}
export default App;
Issues
You've issues declaring React components, several of them aren't using props correctly. function ItemDetail(handleClick) { ... } should be function ItemDetail({ handleClick }) { ... }, and const Discover = (props, params, handleClick) => { ... } should probably be something like const Discover = ({ params, handleClick, ...props }) => { ... }. React components receive a single props object argument.
handleChange in App is also mutating state.
Solution
App
Fix the state mutation and ensure props are passed correctly to routed components. Use an item GUID to search the cart instead of shallow reference equality when checking to add to the cart. When updating cart quantities it is necessary to shallow copy the cart array and cart items that are being updated. Use functional state updates whenever possible so it's ensured it's updating from the previous state and not any stale state value closed over in scope.
function App() {
const [show, setShow] = useState(true);
const [cart, setCart] = useState([]);
const handleClick = (item) => {
// Update cart item quantity if already in cart
if (cart.some((cartItem) => cartItem.productCode === item.productCode)) {
setCart((cart) =>
cart.map((cartItem) =>
cartItem.productCode === item.productCode
? {
...cartItem,
amount: cartItem.amount + 1
}
: cartItem
)
);
return;
}
// Add to cart
setCart((cart) => [
...cart,
{ ...item, amount: 1 } // <-- initial amount 1
]);
};
const handleChange = (productCode, d) => {
setCart((cart) =>
cart.flatMap((cartItem) =>
cartItem.productCode === productCode
? cartItem.amount + d < 1
? [] // <-- remove item if amount will be less than 1
: [
{
...cartItem,
amount: cartItem.amount + d
}
]
: [cartItem]
)
);
};
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="all-categories" element={<AllCategories />} />
<Route path="entertainment" element={<Entertainment />} />
<Route path="login" element={<Login />} />
<Route path="discover" element={<Discover />} />
<Route path="netflix" element={<Netflix />} />
<Route path="orders" element={<Orders />} />
<Route path="sign-up" element={<SignUp />} />
<Route path="products" element={<Products />} />
<Route
path="/itemDetail/:productCode/:value/:vendor"
element={<ItemDetail handleClick={handleClick} />}
/>
<Route
path="/Cart/"
element={(
<Cart
cart={cart}
setCart={setCart}
handleChange={handleChange}
/>
)}
/>
</Routes>
</Router>
);
}
ItemDetail
Access/destructure the handleClick prop correctly. Pass the item's productCode to the callback.
function ItemDetail({ handleClick }) {
const { productCode, vendor, value} = useParams();
const item = { productCode, vendor, value };
return (
<div>
<p>product id: {productCode}</p>
<p>price: {value}</p>
<p>vendor: {vendor}</p>
<button onClick={() => handleClick(item)}>Add to Cart</button>
</div>
);
}
Discover
Correctly access/destructure the handleClick callback. Use the Link component instead of the raw anchor (<a />) tag. The anchor tag will reload the app which very likely isn't what you want to happen. Based on the code I suspect you don't actually need this handleClick since the ItemDetail component is passed it and adds to the cart
import { Link } from 'react-router-dom';
const cardStyle = {
listStyle: "none",
margin: 5,
paddingLeft: 0,
minWidth: 240,
};
const Discover = () => {
const token = "not-the-actual-token";
const [result, setResult] = useState([]);
useEffect(() => {
fetch(
"https://api.flash-internal.flash-group.com/ecommerceManagement/1.0.0/api/product/",
{
method: "GET",
headers: { Authorization: `Bearer ${token}` },
}
)
.then((res) => {
if (!res.ok) {
throw new Error('Network response was not OK');
}
return res.json();
})
.then((data) => setResult(data.result))
.catch(error => {
// handle any rejected Promises, errors, etc...
});
}, []);
return (
<div className="latestdeals container my-5">
<h1>All Products</h1>
<Row className="hotcards">
<Col className="colcard">
{result.map((item) => (
<div key={item.productCode} style={cardStyle}>
<Link to={`/itemDetail/${item.productCode}/${item.value}/${item.vendor}`}>
<StyledCard
name={item.vendor}
title={item.description}
price={item.value}
item={item}
/>
</Link>
</div>
))}
</Col>
</Row>
</div>
);
};
Cart
Don't store the cart total in state, it is easily derived from the cart state.
const Cart = ({ cart, setCart, handleChange }) => {
const handleRemove = (productCode) => {
setCart(cart => cart.filter(item => item.productCode !== productCode));
};
const price = cart.reduce((total, item) => total + item.amount * item.price, 0);
return (
<article>
{cart.map((item) => (
<div className="cart_box" key={item.id}>
<div>
<button onClick={() => handleChange(item.productCode, 1)}>+</button>
<button>{item.amount}</button>
<button onClick={() => handleChange(item.productCode, -1)}>-</button>
</div>
<div>
<span>{item.price}</span>
<button onClick={() => handleRemove(item.productCode)}>Remove</button>
</div>
</div>
))}
<div className="total">
<span>Total Price of your Cart</span>
<span>R - {price}</span>
</div>
</article>
);
};
hello
I am trying to make a menu toggle, where I have a variable with false as initial value, using react createContext and useContext hook, I set the initial state as true
// useMenu Context
import React, { useContext, useState } from 'react'
export const useToggle = (initialState) => {
const [isToggled, setToggle] = useState(initialState)
const toggle = () => setToggle((prevState) => !prevState)
// return [isToggled, toggle];
return { isToggled, setToggle, toggle }
}
const initialState = {
isMenuOpen: true,
toggle: () => {},
}
export const MenuContext = React.createContext(initialState)
const MenuProvider = ({ children }) => {
const { isToggled, setToggle, toggle } = useToggle(false)
const closeMenu = () => setToggle(false)
return (
<MenuContext.Provider
value={{
isMenuOpen: isToggled,
toggleMenu: toggle,
closeMenu,
}}>
{children}
</MenuContext.Provider>
)
}
export default MenuProvider
export const useMenu = () => {
return useContext(MenuContext)
}
so If true it will show the Menu if false it will show the Div where there a div
App.js
const { isMenuOpen } = useMenu()
//the providder
<MenuProvider>
<Header mode={theme} modeFunc={toggleTheme}/>
{isMenuOpen ? (
<Menu />
) : (
<Switch>
<Route path='/writing' component={Writings} />
<Route path='/meta' component={Meta} />
<Route path='/contact' component={Contact} />
<Route path='/project' component={Project} />
<Route exact path='/' component={Home} />
<Route path='*' component={NotFound} />
</Switch>
)}
<Footer />{' '}
</MenuProvider>
and when I add an onclick event the NavLink button of the menu to close it it does not work
Menu
const { closeMenu } = useMenu()
// return statement
{paths.map((item, i) => {
return (
<MenuItem
key={i}
link={item.location}
svg={item.icon}
path={item.name}
command={item.command}
onClick={closeMenu}
/>
)
})}
where did I go wrong
Issue
I suspect the issue is in App where you've a useMenu hook outside the MenuProvider used in App. This useMenu hook is using a MenuContext context but in the absence of a provider it instead uses the default initial context value.
const initialState = {
isMenuOpen: true,
toggle: () => {},
};
export const MenuContext = React.createContext(initialState);
export const useMenu = () => {
return useContext(MenuContext)
};
React.createContext
const MyContext = React.createContext(defaultValue);
Creates a Context object. When React renders a component that
subscribes to this Context object it will read the current context
value from the closest matching Provider above it in the tree.
The defaultValue argument is only used when a component does not
have a matching Provider above it in the tree. This default value can
be helpful for testing components in isolation without wrapping them.
Solution
Since I doubt you want to run/provide more than one menu provider I believe the solution is to move MenuProvider out of and wrap App to provide the context you are updating by nested components.
App.jsx
const { isMenuOpen } = useMenu();
...
<>
<Header mode={theme} modeFunc={toggleTheme}/>
{isMenuOpen ? (
<Menu />
) : (
<Switch>
<Route path='/writing' component={Writings} />
<Route path='/meta' component={Meta} />
<Route path='/contact' component={Contact} />
<Route path='/project' component={Project} />
<Route exact path='/' component={Home} />
<Route path='*' component={NotFound} />
</Switch>
)}
<Footer />
</>
index.jsx (?)
import App from './App.jsx';
...
//the provider
<MenuProvider>
<App />
</MenuProvider>
I want to add to a state array, but I find that any code I write that brings up the state leads to the same error one the browser tries to run it.
Even just a console.log triggers it.
var folderList = this.state.folders;
console.log("folderList: " + folderList);
The full code:
import React, {Component} from 'react';
import {Route, Link} from 'react-router-dom';
import {FontAwesomeIcon} from '#fortawesome/react-fontawesome';
import NoteListNav from '../NoteListNav/NoteListNav';
import NotePageNav from '../NotePageNav/NotePageNav';
import NoteListMain from '../NoteListMain/NoteListMain';
import NotePageMain from '../NotePageMain/NotePageMain';
import dummyStore from '../dummy-store';
import {getNotesForFolder, findNote, findFolder} from '../notes-helpers';
import './App.css';
import AddFolder from '../AddFolder/AddFolder';
import AddNote from '../AddNote/AddNote';
class App extends Component {
state = {
notes: [],
folders: [],
//noteID: 0,
//folderID: 0
};
componentDidMount() {
// fake date loading from API call
setTimeout(() => this.setState(dummyStore), 600);
}
folderSubmit(f){
console.log("folderSubmit ran " + f);
var folderList = this.state.folders;
console.log("folderList: " + folderList);
//this.setState({ folders: joined })
}
renderNavRoutes() {
const {notes, folders} = this.state;
return (
<>
{['/', '/folder/:folderId'].map(path => (
<Route
exact
key={path}
path={path}
render={routeProps => (
<NoteListNav
folders={folders}
notes={notes}
{...routeProps}
/>
)}
/>
))}
<Route
path="/note/:noteId"
render={routeProps => {
const {noteId} = routeProps.match.params;
const note = findNote(notes, noteId) || {};
const folder = findFolder(folders, note.folderId);
return <NotePageNav {...routeProps} folder={folder} />;
}}
/>
<Route path="/add-folder" component={NotePageNav} />
<Route path="/add-note" component={NotePageNav} />
</>
);
}
renderMainRoutes() {
const {notes, folders} = this.state;
return (
<>
{['/', '/folder/:folderId'].map(path => (
<Route
exact
key={path}
path={path}
render={routeProps => {
const {folderId} = routeProps.match.params;
const notesForFolder = getNotesForFolder(
notes,
folderId
);
return (
<NoteListMain
{...routeProps}
notes={notesForFolder}
/>
);
}}
/>
))}
<Route
path="/note/:noteId"
render={routeProps => {
const {noteId} = routeProps.match.params;
const note = findNote(notes, noteId);
return <NotePageMain {...routeProps} note={note} />;
}}
/>
<Route
path="/add-folder"
render={routeProps => {
return <AddFolder addNewFolder={this.folderSubmit}/>
}}
/>
<Route
path="/add-note"
render={routeProps => {
return <AddNote/>
}}
/>
</>
);
}
render() {
return (
<div className="App">
<nav className="App__nav">{this.renderNavRoutes()}</nav>
<header className="App__header">
<h1>
<Link to="/">Noteful</Link>{' '}
<FontAwesomeIcon icon="check-double" />
</h1>
</header>
<main className="App__main">{this.renderMainRoutes()}</main>
</div>
);
}
}
export default App;
this will not be what you expect inside of folderSubmit. How to get around this is discussed in the "Handling Events" part of the documentation. You could bind it to this in the constructor, or use an arrow function as a class field.
folderSubmit = (f) => {
var folderList = this.state.folders;
console.log("folderList:", folderList);
}
Please see this sandbox:
https://codesandbox.io/s/use-context-simple-qygdz?file=/src/App.js
*** You have to go to /check1 to start, and when you reach /check2 there shouldn't be a ddd, but it's still there right now (state not updated)
When I've linked one page to another, the usecontext does not pass the state. Not sure why - but I am glad that with help we were able to pinpoint exactly where the problem is.
maybe it helps if you just use one useState hook from which you update your entire context I included the main parts below (here is a link to a working sample). When i try this i see context changes in every component.
import React from "react";
import "./styles.css";
import ChangeContext from "./components/ChangeContext";
import ViewChange from "./components/ViewChange";
const info = {
artists: null,
messages: null,
songs: null,
userid: "ddd",
accesstoken: null,
refreshtoken: null
};
export const InfoContext = React.createContext();
export default function App() {
const [context, setContext] = React.useState(info);
return (
<InfoContext.Provider value={[context, setContext]}>
<div className="App">
<ChangeContext />
<ViewChange />
</div>
</InfoContext.Provider>
);
}
and then in a component
import React from "react";
import { InfoContext } from "../App";
export default function App() {
const [context, setContext] = React.useContext(InfoContext);
return (
<div className="App">
<h1>{context.userid} uid</h1>
<button
onClick={e => {
setContext({ ...context, userid: 123 });
}}
>
click me
</button>
</div>
);
}
in another component check for changes
import React from "react";
import { InfoContext } from "../App";
export default function ChangeContext() {
const [context, setContext] = React.useContext(InfoContext);
return (
<div className="App">
<h1>{context.userid} uid</h1>
<button
onClick={e => {
setContext({ ...context, userid: 123 });
}}
>
click me
</button>
</div>
);
}
maybe try this instead
const [context, setContext] = useState(info);
return (
<BrowserRouter>
<Route exact path="/signup/:id/:access_token" render={() => <InfoContext.Provider value={[context, setContext]}><Signup /> </InfoContext.Provider>} />
<Route exact path="/" render={() => <Login />} />
<Route exact path="/home/:id/:access_token/:refresh_token" render={() => <Homepage ></Homepage>} />
<Route exact path="/artist/:artistid" render={() => <ArtistPage ></ArtistPage>} />
<Route exact path="/map" render={() => <MapLeaflet />} />
</BrowserRouter>
);
I can't comment yet, but is the userId being updated in the context?
What is the value for console.log(userid) inside artisthomepage.js? Maybe it renders with the old value but then it receives the new one and doesn't re-render the component.
What im trying to achieve in here is to being able to click on a image and render that clicked movie’s info. The problem is the i can not find a way to match id of the clicked movie and the detailed movie. As a result the singleMovierequest has undefined id which causes 404 error. Here is codesandbox link: https://codesandbox.io/s/modern-http-coy0w (Api key is typed as '???' intentionally). Here is movie and app components.
const Movie = (props) => {
const movie = props.singleMovie
const fetchMovie = props.initializeSingleMovie
useEffect(() => { fetchMovie(props.id) }, [props.id])
return (
<div>
<h2>{movie.title}</h2>
<p>{movie.overview}</p>
</div>
)
}
render part of the app component:
<Container>
<h2>Movieapp</h2>
<Router>
<Menu />
<Route exact path="/popular" render={() =>
<PopularMovies />
} />
<Route exact path="/search" render={() =>
<Movies />
} />
<Route exact path="/search/:id" render={(props) => <Movie key={props.match.params.id} />} />
} />
<Route exact path="/popular/:id" render={(props) => <Movie key={props.match.params.id} />} />
</Router>
</Container>
"initializeSingleMovie" is an action,You named it reducer but its an action,for the sake of solving this problem ,you have to use mapDisptachToProps and dispatch(it will access the store methods),below is a modifed Movie.js File.In future have a separate action folder for api hits.Compartmentalise more,hope it helps.
import React from 'react'
import { connect } from 'react-redux'
import { useEffect } from 'react'
import { initializeSingleMovie } from '../reducers/singleMovieReducer'
const Movie = (props) => {
console.log(props,"");
const movie = props.singleMovie
props.initializeSingleMovie(props.id)
return (
<div>
<h2>{movie.title}</h2>
<p>{movie.overview}</p>
</div>
)
}
const mapStateToProps = (state) => {
return {
singleMovie: state.singleMovie
}
}
const mapDispatchToProps = dispatch => {
return {
initializeSingleMovie: (id) => dispatch(initializeSingleMovie(id)),
};
};
export default connect(
mapStateToProps,
mapDisptachToProps
)(Movie)