I am trying to make a simple e-commerce app. When a user goes to cart section and try to increase or decrease quantity, it changes the state but remains same on the page. I need to go back and go cart again to update. How can it change dynamically?
function CardItem() {
const {cart, setCart} = useContext(ProductContext)
const addQuantity = (cartItem) => {
return cart.map((item) => (
cartItem.id === item.id ? item.quantity = item.quantity + 1 : item
))
}
const removeQuantity = (cartItem) => {
cart.map((item) => (
cartItem.id === item.id ? item.quantity = item.quantity - 1 : item
))
}
return (
{
cart.map((cartItem) => (
<tr key={cartItem.id}>
<td class="quantity__item">
<div class="quantity">
<div class="pro-qty-2 d-flex align-items-center justify-content-center text-center">
<button className='increase' onClick={() => removeQuantity(cartItem)}>
-</button>
{cartItem.quantity}
<button className='increase' onClick={() => addQuantity(cartItem)}>+</button>
</div>
</div>
</td>
</tr>
))
})}
Issue
The code in your snippet isn't calling setCart to update any state.
The add/remove quantity handlers are just mutating the cart state object. Don't mutate React state.
Solution
Use the setCart state updater function to enqueue a state update to trigger a rerender to view the updated state. Use a functional state update to correctly update from the previous state and remember that you should shallow copy any state that is being updated.
Example:
function CardItem() {
const { cart, setCart } = useContext(ProductContext);
const addQuantity = (cartItem) => {
setCart(cart => cart.map(item => cartItem.id === item.id
? { // <-- new item object reference
...item, // <-- shallow copy item
quantity: item.quantity + 1, // <-- update property
}
: item
));
};
const removeQuantity = (cartItem) => {
setCart(cart => cart.map(item => cartItem.id === item.id
? {
...item,
quantity: item.quantity - 1,
}
: item
));
};
return (
{cart.map((cartItem) => (
<tr key={cartItem.id}>
<td class="quantity__item">
<div class="quantity">
<div class=" .... ">
<button className='increase' onClick={() => removeQuantity(cartItem)}>
-
</button>
{cartItem.quantity}
<button className='increase' onClick={() => addQuantity(cartItem)}>
+
</button>
</div>
</div>
</td>
</tr>
))
});
}
Since adding/removing is essentially the same action, it's common to use a single function to handle both and pass in the quantity you want to adjust by.
function CardItem() {
const { cart, setCart } = useContext(ProductContext);
const addQuantity = (id, amount) => () => {
setCart(cart => cart.map(item => cartItem.id === id
? {
...item,
quantity: item.quantity + amount,
}
: item
));
};
return (
{cart.map((cartItem) => (
<tr key={cartItem.id}>
<td class="quantity__item">
<div class="quantity">
<div class=" .... ">
<button
className='increase'
onClick={addQuantity(cartItem.id, -1)}
>
-
</button>
{cartItem.quantity}
<button
className='increase'
onClick={addQuantity(cartItem.id, 1)}
>
+
</button>
</div>
</div>
</td>
</tr>
))
});
}
I will recommend using reducers with context to manage state. Something like below a new CartReducer with ADD and REMOVE item etc. actions
const [state, dispatch] = useReducer(CartReducer, initalState);
const addToCart = (id) => {
dispatch({ type: ADD, payload: id});
};
const showHideCart = () => {
dispatch({ type: SHOW, payload:'' });
};
const removeItem = (id) => {
dispatch({ type: REMOVE, payload: id });
};
You can refer this project if it helps
shopping-cart-with-context-api
Related
I have a button that trigger decreaseQuantity function and I have a state,checkoutList comes from context API. When I click that button, its directly decrease the quantity to 1, and what I want is only decrease by one. Why it's not decreasing one by one?
const { checkoutList, setCheckoutList } = useContext(AppContext);
const [uniqueCheckoutList, setUniqueCheckoutList] = useState([]);
const decreaseQuantity = (item) => {
let updatedList = [...uniqueCheckoutList];
let productIndex = updatedList.findIndex((p) => p.id === item.id);
updatedList[productIndex].quantity -= 1;
if (updatedList[productIndex].quantity === 0) {
updatedList = updatedList.filter((p) => p.id !== item.id);
}
setUniqueCheckoutList(updatedList);
setCheckoutList(updatedList);
localStorage.setItem("checkoutList", JSON.stringify(updatedList));
};
Edit: I have a useEffect hook that updates uniqueCheckoutList if the product has the same id. Add quantity property to it so it will just display once. This way I can show the quantity of that product next to the buttons in a span.
useEffect(() => {
let updatedList = [];
checkoutList.forEach((item) => {
let productIndex = updatedList.findIndex((p) => p.id === item.id);
if (productIndex === -1) {
updatedList.push({ ...item, quantity: 1 });
} else {
updatedList[productIndex].quantity += 1;
}
});
setUniqueCheckoutList(updatedList);
}, [checkoutList]);
Here I call the decreaseQuantity function.
<div className="checkout">
{uniqueCheckoutList.length === 0 ? (
<div>THERE IS NO ITEM IN YOUR BAG!</div>
) : (
uniqueCheckoutList.map((item) => {
return (
<div className="container" key={item.id}>
<div>
<img src={item.images[0]} alt={item.title} />
</div>
<div className="texts">
<h1>{item.title} </h1>
<p>{item.description} </p>
<button onClick={() => increaseQuantity(item)}>+</button>
<span>{item.quantity}</span>
<button onClick={() => decreaseQuantity(item)}>-</button>
</div>
</div>
);
})
)}
</div>
I'm building this website with MERN stack and having this rendering bug:
On start, I have a foodList table rendering out all of the food in the database.
I already have a useEffect() with the foodList inside the dependency array - so anytime the users make changes to the foodList table (Add/Edit/Delete), it will instantly render out that added dish without refreshing the page.
When users search for something in this Search & Filter bar, it will hide the foodList table and return a table of searchedFood that is filtered from the foodList array.
But when the users use this Search & Filter functionality and then try to Edit/Delete from that searchedFood table. It won't render the changes instantly - they have to refresh the page to see the changes they made.
This might relate to the useEffect() but I don't know how to apply it for the searchedFood table without disrupting the foodList table.
App.js
export default function App() {
const [foodName, setFoodName] = useState('')
const [isVegetarian, setIsVegetarian] = useState('no')
const [priceRange, setPriceRange] = useState('$')
const [foodUrl, setFoodUrl] = useState('')
const [foodList, setFoodList] = useState([])
const [searchedFood, setSearchedFood] = useState([])
const [noResult, setNoResult] = useState(false)
// Display food list:
useEffect(() => {
let unmounted = false
Axios.get("https://project.herokuapp.com/read")
.then((response) => {
if (!unmounted) {
setFoodList(response.data)
}
})
.catch(error => {
console.log(`The error is: ${error}`)
return
})
return () => {
unmounted = true
}
}, [foodList])
// Add Food to list:
const addToList = async (event) => {//Axios.post logic in here}
// Paginate states:
const [currentPage, setCurrentPage] = useState(1)
const [foodPerPage] = useState(5)
// Get current food:
const indexOfLastFood = currentPage * foodPerPage
const indexOfFirstFood = indexOfLastFood - foodPerPage
const currentFood = foodList.slice(indexOfFirstFood, indexOfLastFood)
const currentSearchedFood = searchedFood.slice(indexOfFirstFood, indexOfLastFood)
const paginate = (pageNumber) => {
setCurrentPage(pageNumber)
}
return (
<section>
<FilterSearch
foodList={foodList}
searchedFood={searchedFood}
setSearchedFood={setSearchedFood}
noResult={noResult}
setNoResult={setNoResult}
paginate={paginate}
/>
{noResult ? <ResultNotFound/>
:
<FoodListTable
foodName={foodName}
priceRange={priceRange}
isVegetarian={isVegetarian}
foodUrl={foodUrl}
foodList={foodList}
currentFood={currentFood}
searchedFood={searchedFood}
currentSearchedFood={currentSearchedFood}
totalFood={foodList.length}
totalSearchedFood={searchedFood.length}
currentPage={currentPage}
paginate={paginate}
noResult={noResult}
foodPerPage={foodPerPage}
/>
}
</section>
)
}
FoodListTable.js
export default function FoodListTable(props) {
return (
<div>
<table>
<thead>
<tr>
<th>
Food name
</th>
<th>Price</th>
<th>
Action
</th>
</tr>
</thead>
<body>
// Return a table with data from searchFood on search:
{props.searchedFood.length > 0 ? props.currentSearchedFood.map((val) => {
return (
<FoodListRow
val={val}
key={val._id}
foodName={val.foodName}
isVegetarian={val.isVegetarian}
priceRange={val.priceRange}
foodUrl={val.foodUrl}
/>
)
}) : props.currentFood.map((val) => { // If not on search, return a table with data from foodList:
return (
<FoodListRow
val={val}
key={val._id}
foodName={val.foodName}
isVegetarian={val.isVegetarian}
priceRange={val.priceRange}
foodUrl={val.foodUrl}
/>
)
})
}
</tbody>
</table>
// Display different Pagination on searched table and food list table:
{props.searchedFood.length > 0 ?
<Pagination foodPerPage={props.foodPerPage} totalFood={props.totalSearchedFood} paginate={props.paginate} currentPage={props.currentPage} />
:<Pagination foodPerPage={props.foodPerPage} totalFood={props.totalFood} paginate={props.paginate} currentPage={props.currentPage} />
}
</div>
)
}
FoodListRow.js
export default function FoodListRow(props) {
// Edit food name:
const [editBtn, setEditBtn] = useState(false)
const handleEdit = () => {
setEditBtn(!editBtn)
}
// Update Food Name:
const [newFoodName, setNewFoodName] = useState('')
const updateFoodName = (id) => {
if (newFoodName) {
Axios.put("https://project.herokuapp.com/update", {
id: id,
newFoodName: newFoodName,
})
.catch(error => console.log(`The error is: ${error}`))
}
}
// Delete food:
const deleteFood = (id) => {
const confirm = window.confirm(`This action cannot be undone.\nAre you sure you want to delete this dish?`);
if(confirm === true){
Axios.delete(`https://project.herokuapp.com/delete/${id}`)
}
}
return (
<tr key={props.val._id}>
<td>
{props.val.foodName}
{editBtn &&
<div>
<input
type="text"
name="edit"
placeholder="New food name.."
autoComplete="off"
onChange={(event) => {setNewFoodName(event.target.value)}}
/>
<button
onClick={() => updateFoodName(props.val._id)}
>
✓
</button>
</div>
}
</td>
<td>{props.val.priceRange}</td>
<td>
<a
href={props.val.foodUrl}
target="_blank"
rel="noopener noreferrer"
>
🔗
</a>
<button
onClick={handleEdit}
>
✏️
</button>
<button
onClick={() => deleteFood(props.val._id)}
>
❌
</button>
</td>
</tr>
);
}
As Mohd Yashim Wong mentioned, we need to re-render every time there's change to the backend.
I ditched the foodList inside the useEffect()'s dependency array and try another method because this is not the correct way to re-render the axios calls. It just keeps sending read requests indefinitely if I use this way. That might be costly.
This is what I have switched to:
I set the dependency array empty
Pull the data from the backend and return it to the frontend after the axios calls
addToList function:
const addToList = async (event) => {
event.preventDefault()
try {
await Axios.post(
"https://project.herokuapp.com/insert",
{
foodName: foodName,
isVegetarian: isVegetarian,
priceRange: priceRange,
foodUrl: foodUrl,
}
)
.then((response) => {
// Return the data to the UI:
setFoodList([...foodList, { _id: response.data._id, foodName: foodName, isVegetarian: isVegetarian, priceRange: priceRange, foodUrl: foodUrl }])
setFoodName('')
setIsVegetarian('no')
setPriceRange('$')
setFoodUrl('')
})
} catch(err) {
console.error(`There was an error while trying to insert - ${err}`)
}
}
updateFoodName function:
const updateFoodName = (id) => {
if (newFoodName) {
Axios.put("https://project.herokuapp.com/update", {
id: id,
newFoodName: newFoodName,
})
.then(() => {
// Update on searchedFood:
props.searchedFood.length > 0 ?
props.setSearchedFood(props.searchedFood.map((val) => {
return (
val._id === id ?
{
_id: id,
foodName: newFoodName,
isVegetarian: props.isVegetarian, priceRange: props.priceRange,
foodUrl: props.foodUrl,
} : val
)
})) //Update on foodList
: props.setFoodList(props.foodList.map((val) => {
return (
val._id === id ?
{
_id: id,
foodName: newFoodName,
isVegetarian: props.isVegetarian, priceRange: props.priceRange,
foodUrl: props.foodUrl,
} : val
)
}))
})
.catch(error => console.log(`Update name failed: ${error}`))
}
}
deleteFood function:
const deleteFood = (id) => {
const confirm = window.confirm(`This action cannot be undone.\nAre you sure you want to delete this dish?`);
if(confirm === true){
Axios.delete(`https://project.herokuapp.com/delete/${id}`)
.then(() => {
props.searchedFood.length > 0
? props.setSearchedFood(props.searchedFood.filter((val) => {
return val._id !== id
}))
: props.setFoodList(props.foodList.filter((val) => {
return val._id !== id
}))
})
}
}
You are never updating the text of the food name. Inside FoodListRow, you should create a state for the name of the food. Set this equal to props.val.foodName and then update it at the end of updateFoodName() after the axios request.
I need some help with incrementing a value through map function while using React's context API. Here is an example to better understand:
Let's say I have items:
const [items, setItems] = useContext(ItemsContext)
These items are JSON objects inside an array.
And then I want to return each item's properties in a list but some of them modified - for example, the item has quantity and I want to increment/decrement it on click. How do I achieve this individually for every item?
I tried making a local state for the quantities:
const [quantity, setQuantity] = useState([])
,so I have all the quantities of all elements but it got me nowhere.
The thing I am trying to accomplish is similar to this:
<div>
<ul>
{
items.map(item => (
<li>
<p>item.name</p>
<p>item.quantity</p>
<button onClick={incQuantity}> </button>
</li>
}
</ul>
</div>
Edit:
const [idCounter, setIdCounter] = useState(0)
I use props. here because this is another component.
const addItem = () => {
if (quantity > 0) {
setIdCounter(idCounter + 1)
setItems(prevItems => [...prevItems, {id: idCounter, name: props.name, price: props.price, quantity: quantity }])
}
}
And I implemented the handler quite the same:
const quantityHandler = (id, diff) => {
setItems(items.map((item) =>
item.id === id ? {...item, quantity: item.quantity + diff} : item
))
}
And here is the list itself:
<div>
<ul>
{
items.map(item => (
<li>
<p>item.name</p>
<p>item.quantity</p>
<button onClick={() => quantityHandler(item.id, 1)}> </button>
<button onClick={() => quantityHandler(item.id, -1)}> </button>
</li>
}
</ul>
</div>
Here is working example and I will explain it a little: in App we make MyContext and state with hook, then we provide state and function to update state to Context provider as value. Then in any place inside Provider we have access to that state and setter. We render items and we can update them using hook setter from Context.
import React, { useState, useContext } from "react";
const MyContext = React.createContext(null);
const initialState = [
{ id: 1, quantity: 1 },
{ id: 2, quantity: 2 },
{ id: 3, quantity: 3 },
{ id: 4, quantity: 4 },
];
const DeepNestedComponent = () => {
const [stateFromContext, setStateFromContext] = useContext(MyContext);
// MyContext should be imported
const buttonHandler = (id, diff) => {
setStateFromContext(
stateFromContext.map((item) =>
item.id === id ? { ...item, quantity: item.quantity + diff } : item
)
);
};
return (
<div>
{stateFromContext.map(({ id, quantity }) => (
<div key={id}>
{quantity}
<button onClick={() => buttonHandler(id, 1)}> + </button>
<button onClick={() => buttonHandler(id, -1)}> - </button>
</div>
))}
</div>
);
};
const App = () => {
const [contextState, setContextState] = useState(initialState);
return (
<MyContext.Provider value={[contextState, setContextState]}>
<DeepNestedComponent />
</MyContext.Provider>
);
};
export default App;
Like it if its is working )
When I add the quantity of product on the product page everything works properly and it transfers me to the cart where it shows how much quantity I have chosen.
When I try to update the product quantity in the cart, it throws me an error.
My CartScreen Code:
function CartScreen(props) {
const cart = useSelector((state) => state.cart);
const { cartItems } = cart;
const productId = props.match.params.id;
const qty = props.location.search
? Number(props.location.search.split("=")[1])
: 1;
const dispatch = useDispatch();
const removeFromCartHandler = (productId) => {
dispatch(removeFromCart(productId));
};
useEffect(() => {
if (productId) {
dispatch(addToCart(productId, qty));
}
}, []);
return (
<div>
<Header />
<div className="cart">
<div className="cart-list">
<ul className="cart-list-container">
<li>
<h3>Shopping Cart</h3>
<div>Price</div>
</li>
{cartItems.length === 0 ? (
<div>Cart is empty</div>
) : (
cartItems.map((item) => (
<li>
<div className="cart-image">
<img src={item.image} alt="product" />
</div>
<div className="cart-name">
<div>
<Link to={"/product/" + item.product}>{item.name}</Link>
</div>
<div>
Qty:
<select
value={item.qty}
onChange={(e) =>
dispatch(addToCart(item.product, e.target.value))
}
>
{[...Array(item.countInStock).keys()].map((x) => (
<option key={x + 1} value={x + 1}>
{x + 1}
</option>
))}
</select>
<button
type="button"
className="button"
onClick={() => removeFromCartHandler(item.product)}
>
Delete
</button>
</div>
</div>
<div className="cart-price">${item.price}</div>
</li>
))
)}
</ul>
</div>
<div className="cart-action">
<h3>
Subtotal ( {cartItems.reduce((a, c) => a + c.qty, 0)} items) : ${" "}
{cartItems.reduce((a, c) => a + c.price * c.qty, 0)}
</h3>
<button
className="button primary full-width"
disabled={cartItems.length === 0}
>
Proceed to Checkout
</button>
</div>
</div>
</div>
);
}
My CartAction Code:
import axios from "axios";
import { CART_ADD_ITEM, CART_REMOVE_ITEM } from "../constants/cartConstants";
const addToCart = (productId, qty) => async (dispatch, getState) => {
try {
const { data } = await axios.get("/api/products/" + productId);
dispatch({
type: CART_ADD_ITEM,
payload: {
product: data._id,
name: data.name,
image: data.image,
price: data.price,
countInStock: data.countInStock,
qty,
},
});
} catch (error) {}
};
const removeFromCart = (productId) => (dispatch) => {
dispatch({ type: CART_REMOVE_ITEM, payload: productId });
};
export { addToCart, removeFromCart };
In CartScreen component what does it say in the console for cartItems ?
Just check your code with useSelector hook and what is it returning.
it must return an object with cartitems key with value as an array.
You could even do this to avoid the error and to define a default value and fix the return object value in useSelector as well.
const { cartItems = [] } = cart;
I know there have been similar questions, but I have a weird issue.
This is what I'm doing
import React, {useState} from 'react';
import './App.css';
import {Table, Button, InputGroup, FormControl} from 'react-bootstrap';
function App() {
const [pons, setPons] = useState();
const [translations, setTranslations] = useState([]);
const [isInEditMode, setIsInEditMode] = useState(false);
const [inputValue, setInputValue] = useState('samochod');
const [errors, setErrors] = useState([]);
const [translationsToSave, setTranslationsToSave] = useState([]);
const changeIsInEditMode = () => setIsInEditMode(!isInEditMode);
const handleEditButtonClick = (id) => console.log('Edit', id);
const handleDeleteButtonClick = (id) => console.log('Delete', id);
const handleInputChange = (e) => setInputValue(e.target.value);
const handleFetchOnButtonClick = async () => {
const resp = await fetch(`http://localhost:8080/pons/findTranslation/${inputValue}`).then(r => r.json()).catch(e => console.log(e));
if (resp.ok === true) {
setTranslations(resp.resp[0].hits);
setErrors([]);
} else {
setErrors(resp.errors ? resp.errors : ['Something went wrong. check the input']);
}
};
const handleSaveTranslations = async () => {
const resp = await fetch('localhost:8080/pons/', {method: 'POST', body: {content: translationsToSave}});
if (resp.ok === true) {
setInputValue('');
setTranslations(null);
}
};
return (
<div className="App">
{errors.length > 0 ? errors.map(e => <div key={e}>{e}</div>) : null}
<InputGroup className="mb-3">
<FormControl
value={inputValue}
onChange={handleInputChange}
placeholder={inputValue}
/>
</InputGroup>
<div className="mb-3">
<Button onClick={handleFetchOnButtonClick} disabled={inputValue === '' || errors.length > 0}>Translate</Button>
<Button onClick={changeIsInEditMode}>
{isInEditMode ? 'Exit edit mode' : 'Enter edit mode'}
</Button>
<Button disabled={translationsToSave.length === 0} onClick={handleSaveTranslations}>Save translations</Button>
</div>
<Table striped bordered hover>
<thead>
<tr>
<th>Original</th>
<th>Translation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{translations ? translations.map(pon => pon.roms.map(rom => rom.arabs.map(arab => arab.translations.map(translation => {
const {source, target} = translation;
return (
<tr>
<td><span dangerouslySetInnerHTML={{__html: source}}/></td>
<td><span dangerouslySetInnerHTML={{__html: target}}/></td>
<td>
{
!translationsToSave.includes(target) ?
<Button onClick={() => {
const tmp = translationsToSave;
tmp.push(target);
setTranslationsToSave(tmp);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
const tmp = translationsToSave;
tmp.splice(tmp.findIndex(elem => elem === target));
setTranslationsToSave(tmp);
}}>
Remove translation
</Button>
}
</td>
</tr>
)
})))) : (
<div>No translations</div>
)}
</tbody>
</Table>
</div>
);
}
export default App;
So it's a basic app, it right now just adds and removes from an array wit setTranslationsToSave. After I click the Add translation button the view stays the same. But it refreshes when I click Enter edit mode. Same with Remove translation. I need to click Enter/Exit edit mode.
Hitting Translate also reloads the view. So the Add/Remove translation buttons are the only ones which do not refresh the page. Why? What am I missing?
The issue is that you are mutating the satte in Add/Remove translation button, so when react check before re-rendering if the state updater was called with the same state it feels that nothing has changed as it does a reference check and ehnce doesn't trigger re-render
Also while updating current state based on previous state use functional callback approach for state updater.
Update your state like below
<Button onClick={() => {
setTranslationsToSave(prev => [...prev, target]);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
setTranslationsToSave((prev) => {
const index = prev.findIndex(elem => elem === target)); return [...prev.slice(0, index), ...prev.slice(index + 1)]
});
}}>
Remove translation
</Button>
In your Add translation click handler, you're mutating the state:
<Button onClick={() => {
// tmp is just a reference to state
const tmp = translationsToSave;
// You are mutating state, this will be lost
tmp.push(target);
setTranslationsToSave(tmp);
}}>
You should duplicate the state and add the new element:
<Button onClick={() => {
setTranslationsToSave([...translationsToSave, target]);
}}>