In my React ECommerce project, I have created Add to cart icon, when clicked disables, the icon is replaced with 'In Cart' text showing that the product is available in cart,but, the problem is when the browser is refreshed the 'In Cart' text disappears and cart icon is back. How to store it in localStorage so that the value remains even when refreshed. Following is the code for reference.
ProductList.js
<ProductConsumer>
{value => {
return value.products.map((product, key) => {
return <Product key={product.id} product={product} />;
});
}}
</ProductConsumer>
Product.js
export default function Product(props) {
// Taken from ProductList.js File
const {id, title, img, price, inCart} = props.product;
<ProductConsumer>
{(value) => (
<button className="cart-btn" disabled={inCart?true:false}
onClick={() => {value.addToCart(id)}}>
{ inCart ? (
<p className="text-capitalize mb-0" disabled>
{" "}
In Cart</p>
) : (
<i className="fas fa-shopping-cart"/>
)}
</button>
)}
</ProductConsumer>
}
context.js (addToCart(id) is defined)
const ProductContext = React.createContext();
class ProductProvider extends Component {
addToCart = (id) => {
let tempProducts = [...this.state.products];
const index = tempProducts.indexOf(this.getItem(id));
const product = tempProducts[index];
// How to store this value 'product.inCart' in...
// ...localStorage and make it true until the product is
// removed
product.inCart = true;
product.count = 1;
const price = product.price;
product.total = price;
this.setState(() => {
return {
products: tempProducts,
cart: [...this.state.cart, product]
};
},
() => {
this.addTotal();
localStorage.setItem('myCart', JSON.stringify(this.state.cart));
});
}
}
const ProductConsumer = ProductContext.Consumer;
export { ProductProvider, ProductConsumer };
As seen above when the product is in cart, cart icon gets disabled, I want to make inCart be true (even when the browser is refreshed) until and unless the product is removed from cart. Watch out for sandbox link: https://codesandbox.io/s/mobile-store-tdgwm
Above File ProductList.js is added and ProductConsumer is defined from context.js
In your setProducts function in context.js
setProducts = () => {
let tempProducts = [];
let activeProducts = JSON.parse(localStorage.getItem("myCart"));
storeProducts.forEach(item => {
let singleItem = { ...item };
if(activeProducts){
activeProducts.forEach(i => {
if (singleItem.id === i.id) {
singleItem = i;
}
});
}
tempProducts = [...tempProducts, singleItem];
});
this.setState(() => {
return { products: tempProducts };
});
};
Working codeSandbox - https://codesandbox.io/s/mobile-store-325x9
Related
I'm trying to make a shopping cart using the useReducer and useContext hook but I'm facing an issue.When I click on Add button I want products to be displayed one under another in the Cart screen but when I add the products in the cart, are not displayed. I don't know what I'm doing wrong. Here is my homepage file where I listed the products :
import React, { useEffect, useReducer, useContext } from 'react';
import '../components/components.css';
import axios from 'axios';
import logger from 'use-reducer-logger';
import { Store } from '../Store';
const { state, dispatch: ctxDispatch } = useContext(Store);
const { cart } = state;
const addToCartHandler = () => {
const existItem = cart.cartItems.find((x) => x._id === product.id);
const quantity = existItem ? existItem.quantity + 1 : 1;
ctxDispatch({
type: 'CART_ADD_ITEM',
payload: { ...product, quantity },
});
};
export default Products;
If somebody can give me a tip it would be nice
in my think, existItem is going to be changed as following:
const addToCartHandler = (item) => {
const existItem = cart.cartItems.find((x) => x._id === item.id);
const quantity = existItem ? existItem.quantity + 1 : 1;
ctxDispatch({
type: 'CART_ADD_ITEM',
payload: { ...item, quantity },
});
};
...
};
you have some issue with your logic in the dispatch to add to cart I made the respective modifications:
const addToCartHandler = (item) => {
const existItem = cart.cartItems.find((x) => x._id === item.id);
const quantity = existItem ? existItem.quantity + 1 : 1;
ctxDispatch({
type: 'CART_ADD_ITEM',
payload: { ...item, quantity },
});
};
return (
<div className="produse">
{products.map((item) => (
<div className="produs" key={item._id}>
<div>
<img className="imagine-produs" src={item.image} alt={item.name} />
</div>
<div>
<h3 className="nume-produs">{item.name} </h3>
</div>
<div className="pret-produs">{item.price} RON</div>
<div>
<button className="adauga-produs" onClick={()=>addToCartHandler(item)}>
Adauga
</button>
</div>
</div>
))}
</div>
);
};
Be aware that in order to be able to run it locally I needed to drop logger
So i wanna display my cart item in the cart list, as i have set the initial state of cart to storeProducts, 8 of my products should be rendered as i'm using the map function. I haven't made the ui of my cartItem.js yet, but instead, i should have 8 lines of text as "this is a cart item" from my cartItem.js. Please help me to find out what's wrong with my codes! Thank you so much!
context.js:
class ProductProvider extends React.Component {
state = {
products: storeProducts,
detailProduct: detailProduct,
cart: storeProducts,
modalOpen: false,
modalProduct: detailProduct
};
getItem = (id) => {
const product = this.state.products.find((item) => item.id === id);
return product;
};
addToCart = (id) => {
let tempProducts = [...this.state.products];
const index = tempProducts.indexOf(this.getItem(id));
const product = tempProducts[index];
product.inCart = true;
product.count = 1;
const price = product.price;
product.total = price;
this.setState(() => {
return (
{ products: tempProducts, cart: [...this.state.cart, product] },
() => console.log(this.state)
);
});
};
openModal = (id) => {
const product = this.getItem(id);
this.setState(() => {
return { modalProduct: product, openModal: true };
});
};
closeModal = (id) => {
this.setState(() => {
return { modalOpen: false };
});
};
render() {
return (
<ProductContext.Provider
value={{
...this.state,
addToCart: this.addToCart,
openModal: this.openModal,
closeModal: this.closeModal
}}
>
{this.props.children}
</ProductContext.Provider>
);
}
}
CartItem.js:
import React from "react";
function CartItem(item) {
return <div>this is a cart item</div>;
}
export default CartItem;
CartList.js:
import React from "react";
import CartItem from "./CartItem"
export default function CartList (props) {
const {cart} = props
return (
<div>
{cart.map((item) => (
<CartItem key={item.id} item={item} />
))}
</div>
)
}
Sandbox link:https://codesandbox.io/s/cart-code-addict-buz0u?file=/src/App.js
In your Sandbox link, EmptyCart component can not found, so the app is error.
And your should add ProductConsumer too.
'PropTypes' is defined but never used. (no-unused-vars)
eslint
'useParams' is defined but never used. (no-unused-vars)
eslint
'Modal' is defined but never used. (no-unused-vars)
eslint
Missing radix parameter. (radix)
eslint
'ProductConsumer' is not defined. (react/jsx-no-undef)
eslint
I'm creating a movie app using React/Redux that allows the user to choose a movie by clicking on a button (buy a ticket ) that takes the user to another page to choose the quantity of the ticket and add to the cart his purchase.
The idea is when the user clicks on the button add to card, I want two things to happen one is that the quantity should be updated in the badge cart in the navBar and also add this the movie to the bag if the user wants to checkout.
How can I create a state called cart in reducers to update the quantity and to add the movie into that cart when I click on add to a cart?
What do you think?
<-- Action types -->
export const ActionsTypes = {
SET_MOVIES : "SET_MOVIES",
GET_MOVIE : "GET_MOVIE",
REMOVE_MOVIE : "REMOVE_MOVIE",
QUANTITY: "QUANTITY",
}
<-- Quantity action-->
export const MovieQuantity = () => {
return {
type : ActionsTypes.QUANTITY
}
}
<-- Reducers -->
const initialState = {
movies: [],
};
//Movies Reducers
export const setMoviesReducers = (state = initialState, action) => {
switch (action.type) {
case ActionsTypes.SET_MOVIES:
return {...state, movies: action.payload }
default:
return state;
}
}
// single Movies Reducers
export const GetMovieDetailsReducers = (state={}, action) => {
switch(action.type) {
case ActionsTypes.GET_MOVIE :
return {...state, ...action.payload}
case ActionsTypes.REMOVE_MOVIE :
return {};
default :
return state
}
}
export const movieQuantityReducers = (state = 0 , action) => {
switch(action.type) {
case ActionsTypes.QUANTITY:
return state + 1;
default :
return state;
}
}
<-- Movie Details add to cart component -->
const MovieDetails = () => {
const [quantity, setQuantity] = useState(1)
const singleMovie = useSelector((state)=> state.movie);
const quantityBag = useSelector((state)=> state.quantity);
const {title, poster_path, overview} = singleMovie;
const dispatch = useDispatch();
let {movieId} = useParams();
// Handle Click Quantity
const handleQuantity = (type) => {
if(type === "dec") {
quantity > 1 && setQuantity(quantity - 1)
} else {
setQuantity(quantity + 1)
}
}
// add to cart Handler
const CartHandler = () => {
dispatch(MovieQuantity(quantityBag)) // the quantity is just incrementing
}
// Get a single Product & Remove product
useEffect(()=> {
try {
const getSingleMovie = async () => {
const request = await axios.get(`https://api.themoviedb.org/3/movie/${movieId}?api_key=&&&&&`);
const response = await request.data;
dispatch(getMovie(response))
}
getSingleMovie();
} catch(error) {
console.log(`ERROR : ${error}`)
}
//Clean up
return () => {
dispatch(removeMovie());
}
}, [movieId])
return (
<section className="movieDetails_container">
<div className="wrapper">
<div className="img-container">
<img src={`${ImgPath}` + poster_path} alt={title}/>
</div>
<div className="info-container">
<h1>{title}</h1>
<p>{overview}</p>
<div className="quantity-container">
<Remove className="quantity-icon" onClick={()=> handleQuantity("dec")}/>
<span className="amount">{quantity}</span>
<Add className="quantity-icon" onClick={()=> handleQuantity("incr")}/>
</div>
<button className="btn-add" onClick={()=> CartHandler()}>Add To Cart</button>
</div>
</div>
</section>
)
}
export default MovieDetails
<-- navBar componenet -->
const Navbar = () => {
const quantityBag = useSelector((state)=> state.quantity);
return (
<nav className="navBar-section">
<Link to="/">
<h1 className="logo">映画館</h1>
</Link>
<Badge badgeContent={quantityBag} color="primary">
<LocalMall className="icon-bag" />
</Badge>
</nav>
)
}
export default Navbar
I'd model the state differently. From what you're describing, the following would work nicely:
const state = {
// movie details reducer handles this part:
movies: {
"movie1Id": {
"name": "Title of movie1"
// more movie details here
},
"movie2Id": {
"name": "Title of movie2"
// more movie details here
},
},
// cart reducer handles this part:
cart: {
"movie1Id": 3, // quantity. user chose 3 tickets for movie1.
"movie2Id": 1, // quantity. user chose 1 ticket for movie2.
}
};
const setCartItemAction = (movieId, quantity) => ({
type: "SET_CART_ITEM",
payload: { movieId, quantity }
});
The setCartItemAction would be enough to model your use case. Calling it with a quantity of 0 for a certain movie would be the same as removing the movie from the cart.
I would like your take on a specific implementation. I have a react app (no redux), the app has a shopping cart. The shopping cart is defined in the state in the App component and it is passed and used further down the tree in several components. E.g. I have a component called ShoppingCart, it displays the shopping cart, plus it has actions to add/remove/clear the cart.
My problem is updating the shopping cart state after performing an action on the shopping cart. E.g. when I call a function to clear the shopping cart, the state should be updated in the App component thus updating my component which is further down the tree. How would one implement these action functions (without redux)?
Code:
const App = () => {
const [cart, setCart] = useState({ lines: [], total: 0 });
return <ShoppingCart cart={cart} />;
}
const ShoppingCart = ({ cart }) => {
const onAddOne = l => {
// not sure how to update cart and update state
}
const onRemoveOne = l => {
// not sure how to update cart and update state
}
return (
<table>
{
cart.lines.map(l => <tr><td>{l.name}</td><td><button onClick={() => onAddOne(l)}>+</button><button onClick={() => onRemoveOne(l)}>-</button></td></tr>)
}
</table>
);
}
Thanks in advance for any tip.
Here you can use the useContext hook.
The idea is similar to redux.
So, what you can do is, first create a StateProvider, like in the example
import React, { createContext, useReducer, useContext } from "react";
export const StateContext = createContext();
export const StateProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
);
export const useStateValue = () => useContext(StateContext);
Similarly, create a Reducer for that, you can add more reducers, the example shown is to ADD ITEMS IN BASKET and REMOVE ITEMs FROM BASKET
export const initialState = {
basket: [],
user: null,
};
export const getBasketTotal = (basket) =>
basket?.reduce((amount, item) => item.price + amount, 0);
function reducer(state, action) {
switch (action.type) {
case "ADD_TO_BASKET":
return { ...state, basket: [...state.basket, action.item] };
case "REMOVE_ITEM":
let newBasket = [...state.basket];
const index = state.basket.findIndex(
(basketItem) => basketItem.id === action.id
);
if (index >= 0) {
newBasket.splice(index, 1);
} else {
console.warn("Cant do this");
}
return { ...state, basket: newBasket };
default:
return state;
}
}
export default reducer;
Go to your index.js file and wrap your file like this
<StateProvider initialState={initialState} reducer={reducer}>
<App />
</StateProvider>
And voila, while adding items to the basket use following code
const addtobasket = () => {
dispatch({
type: "ADD_TO_BASKET",
item: {
id: id,
title: title,
price: price,
rating: rating,
color: color,
},
});
};
I found a solution, however, I am not sure it is the correct way to do things:
const App = () => {
const onUpdateCart = (cart) => {
setCart({ ...cart });
}
const [cart, setCart] = useState({ lines: [], total: 0, onUpdateCart });
return <ShoppingCart cart={cart} />;
}
const ShoppingCart = ({ cart }) => {
const onRemoveLine = l => {
cart.lines = cart.lines.filter(l2 => l2 !== l);
cart.onUpdateCart(cart);
}
const onAddOne = l => {
l.amount++;
cart.onUpdateCart(cart);
}
const onRemoveOne = l => {
l.amount--;
cart.onUpdateCart(cart);
}
return (
<table>
{
cart.lines.map(l => (
<tr>
<td>{l.name}</td>
<td>
<button onClick={() => onAddOne(l)}>+</button>
<button onClick={() => onRemoveOne(l)}>-</button>
<button onClick={() => onRemoveLine(l)}>x</button>
</td>
</tr>)
)
}
</table>
);
};
The straight forward way to implement this is to pass down props to the child component that when called update the state.
Notice how all state business logic is in a central place .e.g in App component. This allows ShoppingCart to be a much simpler.
const App = () => {
const [cart, setCart] = useState({ lines: [], total: 0 });
const updateLineAmount = (lineIdx, amount) => {
// update the amount on a specific line index
setCart((state) => ({
...state,
lines: state.lines.map((line, idx) => {
if (idx !== lineIdx) {
return line;
}
return {
...line,
amount: line.amount + amount,
};
}),
}));
};
const onAddOne = (lineIdx) => {
updateLineAmount(lineIdx, 1);
};
const onRemoveOne = (lineIdx) => {
updateLineAmount(lineIdx, -1);
};
return (
<ShoppingCart cart={cart} onAddOne={onAddOne} onRemoveOne={onRemoveOne} />
);
};
const ShoppingCart = ({ cart, onAddOne, onRemoveOne }) => {
return (
<table>
{cart.lines.map((line, idx) => (
<tr key={idx}>
<td>{line.name}</td>
<td>
<button onClick={() => onAddOne(idx)}>+</button>
<button onClick={() => onRemoveOne(idx)}>-</button>
</td>
</tr>
))}
</table>
);
};
I need fresh eyes on this. As I am slowly learning React and Redux i have run into a roadblock again.
/actions/items.js
export const DELETE_ITEM = "DELETE_ITEM"
export function deleteItem(id) {
return {
type: DELETE_ITEM,
id
}
}
/components/Item.jsx
export default class Item extends React.Component {
renderDelete = () => {
return <button onClick={this.props.onDelete}>x</button>
};
renderItem = () => {
const onDelete = this.props.onDelete
return (
<div onClick={this.edit}>
<span>{this.props.text}</span> {onDelete ? this.renderDelete() : null}
</div>
)
}
/components/Items.jsx
export default class Items extends React.Component {
handleOnDelete = (id) => {
this.props.dispatch(actions.deleteItem(id))
}
render() {
const {items, onEdit, onDelete } = this.props
return (
<ul>{items.map(item =>
<li key={item.id}>
<Item
id={item.id}
text={item.text}
onEdit={this.handleOnEdit}
onDelete={this.handleOnDelete.bind(null, item.id)}
/>
</li>
)}</ul>
);
}
}
export default connect(
state => ({
items: state.items
})
)(Items)
/reducers/items.js
case types.DELETE_ITEM:
const filteredItems = state.filter((item) => {
item.id !== action.id
});
return filteredItems
I'm not sure why clicking on x button to delete an item deletes all of them. Thanks in advance for the help
You do not return value in filter in your reducers.
Your should add return:
const filteredItems = state.filter((item) => {
return item.id !== action.id;
});
Or use short version, without brackets:
const filteredItems = state.filter((item) => item.id !== action.id);