I am trying to add items to a cart without redux. I works but I am dont think I did it properly because I get a weird quantity value.
How it works so far.
You click an item button it passes in props to the handleAddCartItem(). The props are addedItem which is the name of the item and addedItemPrice.
The default state
state = {
term: "",
cart: [
{
item: "",
price: 0,
quantity: 0
}
]};
how handler works.
handleAddCartItem = (addedItem, addedItemPrice) => {
// check if already in cart
let index = this.state.cart.findIndex(el => el.item === addedItem);
if (index == -1) {
console.log("item new to cart");
this.setState(prevState => ({
cart: [
...prevState.cart,
{
item: addedItem,
price: addedItemPrice,
quantity: 1
}
]
}));
} else {
this.setState(prevState => ({
quantity: (prevState.cart[index].quantity += 1)
}));
} };
The weird extra quantity state screenshot.
I am new to react so the code is probably gross sorry.
My logic in the handler
- checks in item name is already inside the cart array.
-if it newly added item then new object is added to array.
-if item is already in array then I use the index of the item currently inside the array and only update its quantity.
I don't understand why I have an extra quantity state added =/
Any advice appreciated thank you.
I belive code speaks for itself but if you're confused about something, ask me.
handleAddCartItem = (addedItem, addedItemPrice) => {
// check if already in cart
let index = this.state.cart.findIndex(el => el.item === addedItem);
if (index == -1) {
console.log("item new to cart");
this.setState(prevState => ({
cart: [
...prevState.cart,
{
item: addedItem,
price: addedItemPrice,
quantity: 1
}
]
}));
} else {
// THIS IS THE PROBLEM
this.setState(prevState => ({
quantity: (prevState.cart[index].quantity += 1)
}));
// HOW IT SHOULD BE
this.setState(prevState => {
let newCart = [...prevState.cart];
newCart[index].quantity += 1;
return {cart: newCart};
});
}
}
quantity in the necessary item object needs to be incremented. But instead another variable quantity was created at the root. Try updating the else block to this :
else {
this.setState(prevState => ({
cart: prevState.cart.map((item, itemIndex) => {
if(itemIndex === index) {
return {
...item,
quantity: item.quantity + 1
}
}
return item
})
}));
}
Related
So basically I am making a shopping cart and I want to add a functionality if an item is already in the cart then increase it's quantity by 1. If you add same item and they have different sizes then show them separetely. I managed to deal with increasing the quantity in my reducer's logic but when I add another block condition it doesn't work. Here is the code:
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
bagData: [],
};
export const bagDataSlice = createSlice({
name: "bagDataProducts",
initialState,
reducers: {
sendDataToCardComponent: (state, action) => {
let { id, size } = action.payload;
const findItemById = state.bagData.find(
(item) => item.id === id && item.size === size
);
if (findItemById) {
state.bagData.filter((item, i) => (state.bagData[i].quantity += 1));
} else {
state.bagData.push({ ...action.payload, quantity: 1 });
}
},
increaseItemQuantity: (state, { payload }) => {
state.bagData.filter((item, i) =>
item.id === payload ? (state.bagData[i].quantity += 1) : item
);
},
decreaseItemQuantity: (state, { payload }) => {
state.bagData.filter((item, i) =>
item.id === payload && item.quantity > 1
? (state.bagData[i].quantity -= 1)
: item
);
},
removeItem: (state, { payload }) => {
state.bagData = state.bagData.filter((item) => item.id !== payload);
},
},
});
when I add the condition of
const findItemById = state.bagData.find(
(item) => item.id === id && item.size === size
);
it only increments the item without checking it's size, only checks it's id even though there are 2 conditions for that function. Could you please explain that to me?
state.bagData.filter((item, i) => (state.bagData[i].quantity += 1));
For your first case, this is updating every item's quantity if you found a matching item by id and size. Since you've already found the item and stored it in findItemById, you should be able to use the following.
Caveat, Immer only supports mutating array elements by index so use findIndex() instead of find().
const itemIndex = state.bagData.findIndex(
(item) => item.id === id && item.size === size
);
if (itemIndex !== -1) {
state.bagData[itemIndex].quantity++;
} else {
state.bagData.push({ ...action.payload, quantity: 1 });
}
Here's a quick demo showing that this works
const initialState = {
bagData: [{
id: 1,
quantity: 1,
size: "S"
}]
};
const sendDataToCardComponent = (action) =>
immer.produce(initialState, (state) => {
let { id, size } = action.payload;
const itemIndex = state.bagData.findIndex(
(item) => item.id === id && item.size === size
);
if (itemIndex !== -1) {
state.bagData[itemIndex].quantity++;
} else {
state.bagData.push({ ...action.payload, quantity: 1 });
}
});
console.log(
"increment existing",
sendDataToCardComponent({ payload: { id: 1, size: "S" } })
);
console.log(
"add new",
sendDataToCardComponent({ payload: { id: 1, size: "M" } })
);
.as-console-wrapper { max-height: 100% !important; }
<script src="https://cdn.jsdelivr.net/npm/immer"></script>
As mentioned in the comments, you're misusing Array.prototype.filter() which should only be used to return a new array with items filtered in or out based on a predicate. Your code can be cleaned up somewhat
increaseItemQuantity: (state, { payload }) => {
const found = state.bagData.findIndex(({ id }) => id === payload);
if (found !== -1) {
state.bagData[found].quantity++;
}
},
decreaseItemQuantity: (state, { payload }) => {
const found = state.bagData.findIndex(({ id }) => id === payload);
if (found !== -1) {
state.bagData[found].quantity--;
}
},
Your last reducer is using filter() correctly but Immer also supports splice()
removeItem: (state, { payload }) => {
const found = state.bagData.findIndex(({ id }) => id === payload);
if (found !== -1) {
state.bagData.splice(found, 1);
}
},
I'm trying to implement a shopping cart by using contexts. The context works fine, but I need to create a method that removes one product each click. To do that I have to use useState() function. However, it doesn't even change the data in the state.
Here is my code for the function. Let me know if something isn't clear
For more info. productsToPurchase is an array that have all the purchased products which looks like that:
[{itemId: "ps-5", qty:2},{itemId: "iphone-xr", qty:4},{itemId:"ps-4", qty:7}]
export const CartContext = createContext()
class CartContextProvider extends Component {
state = {
productsToPurchase: [],
totalPrice:[]
}
addProduct = (itemId, prices) => {
this.setState(oldState => {
const objWithIdExist = oldState.productsToPurchase.find((o) => o.itemId === itemId);
return {
productsToPurchase: !objWithIdExist
? [...oldState.productsToPurchase, { itemId, qty: 1, price: prices }]
: oldState.productsToPurchase.map((o) =>
o.itemId !== itemId ? o : { ...o, qty: o.qty + 1 }
)
}
})
this.setState(oldState=>{
return{
totalPrice: getTotalAmount(oldState.productsToPurchase)
}
})
}
decreaseProduct = (itemId) =>{
this.setState(oldState=>{
// catch the item by mapping
return oldState.productsToPurchase.map(product=>{
if (product.itemId === itemId){
if (product.qty === 1){
//remove the product from the list
console.log("qty is 1!")
}
else {
console.log("Quantity is more than 1")
return {...product, qty: product.qty - 1}
}
}
})
})
}
render() {
return (
<CartContext.Provider value={{...this.state, addProduct: this.addProduct, decreaseProduct: this.decreaseProduct}}>
{this.props.children}
</CartContext.Provider>
)
}
}
function getTotalAmount(productsToPurchase) {
const totalPrice = []
productsToPurchase.map((product, productIndex) =>{
product.price.map((price, priceIndex)=>{
if (productIndex === 0){
totalPrice.push({currency: price.currency, amount: price.amount * product.qty})
}
else {
totalPrice.map(total=>{
if (total.currency === price.currency){
total.amount = total.amount + (price.amount * product.qty)
}
})
}
})
})
return totalPrice
}
export default CartContextProvider;
If you want to subtract 1 from the selected product quantity and remove it if it hits 0, try this
// Make sure you return the new state object in its entirety
this.setState(({ productsToPurchase, ...oldState }) => ({
...oldState,
productsToPurchase: productsToPurchase.reduce((arr, product) => {
// is this the selected product?
if (product.itemId === itemId) {
// product qty > 1, include it with decrement
if (product.qty > 1) {
return [...arr, {
...product,
qty: product.qty - 1
}]
}
return arr // otherwise exclude this product
}
// otherwise, just include this product
return [...arr, product]
}, [])
}))
I'm having some trouble with the React useState hook. I have a todolist with a checkbox button and I want to update the 'done' property to 'true' that has the same id as the id of the 'clicked' checkbox button. If I console.log my 'toggleDone' function it returns the right id. But I have no idea how I can update the right property.
The current state:
const App = () => {
const [state, setState] = useState({
todos:
[
{
id: 1,
title: 'take out trash',
done: false
},
{
id: 2,
title: 'wife to dinner',
done: false
},
{
id: 3,
title: 'make react app',
done: false
},
]
})
const toggleDone = (id) => {
console.log(id);
}
return (
<div className="App">
<Todos todos={state.todos} toggleDone={toggleDone}/>
</div>
);
}
The updated state I want:
const App = () => {
const [state, setState] = useState({
todos:
[
{
id: 1,
title: 'take out trash',
done: false
},
{
id: 2,
title: 'wife to dinner',
done: false
},
{
id: 3,
title: 'make react app',
done: true // if I checked this checkbox.
},
]
})
You can safely use javascript's array map functionality since that will not modify existing state, which react does not like, and it returns a new array. The process is to loop over the state's array and find the correct id. Update the done boolean. Then set state with the updated list.
const toggleDone = (id) => {
console.log(id);
// loop over the todos list and find the provided id.
let updatedList = state.todos.map(item =>
{
if (item.id == id){
return {...item, done: !item.done}; //gets everything that was already in item, and updates "done"
}
return item; // else return unmodified item
});
setState({todos: updatedList}); // set state to new object with updated list
}
Edit: updated the code to toggle item.done instead of setting it to true.
You need to use the spread operator like so:
const toggleDone = (id) => {
let newState = [...state];
newState[index].done = true;
setState(newState])
}
D. Smith's answer is great, but could be refactored to be made more declarative like so..
const toggleDone = (id) => {
console.log(id);
setState(state => {
// loop over the todos list and find the provided id.
return state.todos.map(item => {
//gets everything that was already in item, and updates "done"
//else returns unmodified item
return item.id === id ? {...item, done: !item.done} : item
})
}); // set state to new object with updated list
}
const toggleDone = (id) => {
console.log(id);
// copy old state
const newState = {...state, todos: [...state.todos]};
// change value
const matchingIndex = newState.todos.findIndex((item) => item.id == id);
if (matchingIndex !== -1) {
newState.todos[matchingIndex] = {
...newState.todos[matchingIndex],
done: !newState.todos[matchingIndex].done
}
}
// set new state
setState(newState);
}
Something similar to D. Smith's answer but a little more concise:
const toggleDone = (id) => {
setState(prevState => {
// Loop over your list
return prevState.map((item) => {
// Check for the item with the specified id and update it
return item.id === id ? {...item, done: !item.done} : item
})
})
}
All the great answers but I would do it like this
setState(prevState => {
...prevState,
todos: [...prevState.todos, newObj]
})
This will safely update the state safely. Also the data integrity will be kept. This will also solve the data consistency at the time of update.
if you want to do any condition do like this
setState(prevState => {
if(condition){
return {
...prevState,
todos: [...prevState.todos, newObj]
}
}else{
return prevState
}
})
I would create just the todos array using useState instead of another state, the key is creating a copy of the todos array, updating that, and setting it as the new array.
Here is a working example: https://codesandbox.io/s/competent-bogdan-kn22e?file=/src/App.js
const App = () => {
const [todos, setTodos] = useState([
{
id: 1,
title: "take out trash",
done: false
},
{
id: 2,
title: "wife to dinner",
done: false
},
{
id: 3,
title: "make react app",
done: false
}
]);
const toggleDone = (e, item) => {
const indexToUpdate = todos.findIndex((todo) => todo.id === item.id);
const updatedTodos = [...todos]; // creates a copy of the array
updatedTodos[indexToUpdate].done = !item.done;
setTodos(updatedTodos);
};
I'm revisiting an old problem that I asked about and approaching it differently. Currently I want to update the score array individual. Currently what happens is when the onClick function runs it removes all whole array. How do I update just that array's index that I am trying to specify???
class App extends React.Component {
constructor(props) {
super(props);
this.scoreFive = this.scoreFive.bind(this);
this.state = {
score: [10, 20]
}
}
scoreFive(key) {
this.setState((prevState) => {
return {
score: [
prevState.score[key] + 5
]
}
})
console.log(key)
}
render() {
return (
<div>
<h1>Dominoes</h1>
<Player key={1} name="micah" score={this.state.score[0]} scoreFive={() => this.scoreFive(0)} />
<Player key={2} name="kyndra" score={this.state.score[1]} scoreFive={() => this.scoreFive(1)} />
</div>
);
}
}
const newArray = this.state.score.map(element => element + 5);
And then do:
this.setState({score: newArray});
The map function returns a new array using your condition.
Any problem let me know :)
You have to take the array from the previous state, clone it, modify the certain index, and then update the state with that:
score: prevState.score.map((value, index) => index === key ? value + 5 : value)
If you do that quite often it is quite repetetive, you could also abstract that into a helper:
const lens = (key, cb) => obj => ({ ...obj, [key]: cb(obj[key]) });
const index = (index, cb) => array => array.map((v, i) => i === index ? cb(v) : v);
Usable as:
this.setState(lens("score", index(key, it => it + 5)));
Try:
scoreFive(index) {
this.setState((prevState) => {
const score = prevState.score; // reference the array
score[index] + 5; // modify the specific index
return {
score: score
};
})
console.log(key)
}
update the score and setState it..
scoreFive(key) {
let {score} = this.state;
score[key] += 5;
this.setState({score});
}
Edited----------------------
So after researching and some negative marking i found that i was doing it wrong and mutating the state as mentioned in the-power-of-not-mutating-data
So here is the updated implementation
scoreFive(key) {
this.setState({score: this.state.score.map((data, index) => index === key ? data + 5 : data) });
}
Thanks for helping me out :)
How can I update isSelected to true for the item that the user clicks on?
Here is my handleSelect function so far:
handleSelect(i) {
//Get Genres
let genres = this.state.allGenres;
genres = genres.map((val, index) => {
//val.isSelected = index === true;
return val;
});
//Set State
this.setState({
allGenres: genres
})
}
This function is passed down correctly via props and is working, the issue I'm having is with the logic.
The current logic sets all items in the array to true which is incorrect. I only want to update / toggle the state of the item the user clicked.
I have an array of objects within state, here is what one of the objects looks like:
state = {
allGenres: [
{
id: 1,
genreTitle: 'Horror',
isSelected: false,
}
]
}
Here's how we can do it:
On each clicked Genre, we get its id.
After we have the id, then we toggle the selected genre isSelected flag.
Please follow updateGenres method, to check how we did it in an immutable way.
updateGenres(selectedId) {
const { allGenres } = this.state
this.setState({
// Here we make sure we don't mutate the state
allGenres: allGenres.map(genre => ({
...genre,
// Toggle the clicked one, and reset all others to be `false`.
isSelected: genre.id === selectedId
// If you want to keep the values of the rest genres, then the check should be:
// isSelected: (genre.id === selectedId) ? !genre.isSelected : genre.isSelected
}))
})
}
renderGenres() {
const { allGenres } = this.state
return allGenres.map(genre => <Gengre onClick={() => this.updateGenres(genre.id) })
}
render() {
return <div>{this.renderGenres()}</div>
}
The benefit of this approach is that you don't depend on the index of the allGenres and your toggle implementation will be decoupled to the presentation. Imagine if you change the genres sort (from ASC to DESC), then you have to change your toggle logic too.
Easiest way is that maintain allGenres and selectedGenre separately in state
So handleSelect will be like
handleSelect(id) {
this.setState({
selectedGenre: id
});
}
If the i passed to handleSelect is the index of the genre in the genres array then
handleSelect(i) {
//Get Genres
let genres = this.state.allGenres;
genres = genres.map((val, index) => {
val.isSelected = index === i;
return val;
});
//Set State
this.setState({
allGenres: genres
})
}
if the i refers to the id of the genre then
handleSelect(i) {
//Get Genres
let genres = this.state.allGenres;
genres = genres.map((val) => {
val.isSelected = val.id === i;
return val;
});
//Set State
this.setState({
allGenres: genres
})
}
I'll assume that you have some components being rendered, and that you want to click in these components... You could use bind and pass to the callback function the index of the array... for example:
class Component extends React.Component {
constructor() {
super();
this.state = {
allGenres: [{
id: 1,
genreTitle: 'Horror',
isSelected: false
}]
}
}
handleSelect(i) {
//Get Genres
let genres = this.state.allGenres;
genres = genres.map((val, index) => {
val.isSelected = index === i;
return val;
});
//Set State
this.setState({
allGenres: genres
})
}
render() {
const {allGenres} = this.state;
return (
<ul>
:D
{allGenres.map((genre, i) => (
<li onClick={this.handleSelect.bind(this, i)}>
{genre.genreTitle} {genre.isSelected && 'Is Selected'}
</li>
))}
</ul>
);
}
}
I'm using bind in the onClick callback from the li, this way I have access to the genre index in your callback.
There are some other ways of doing this though, since you have the id of the genre, you might want to use that instead of the index, but it's up to you to decided.