React. How to update the state of a clicked element - reactjs

I have some elements inside an array that shares a same state. I need to update only the clicked one in order to add one more item to my shopping cart. How can i do this without changing the others?
My initial state looks like this:
class ShoppingCart extends Component {
constructor() {
super();
this.state = {
isEmpty: true,
cartItems: [],
count: 0,
};
this.getStoredProducts = this.getStoredProducts.bind(this);
this.handleButtonIncrease = this.handleButtonIncrease.bind(this);
}
componentDidMount() {
this.getStoredProducts();
}
handleButtonIncrease() {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
}
getStoredProducts() {
const getFromStorage = JSON.parse(localStorage.getItem('cartItem'));
if (getFromStorage !== null) {
this.setState({
cartItems: getFromStorage,
}, () => {
const { cartItems } = this.state;
if (cartItems.length) {
this.setState({ isEmpty: false });
}
});
}
}
render() {
const { isEmpty, cartItems, count } = this.state;
const emptyMsg = (
<p data-testid="shopping-cart-empty-message">Seu carrinho está vazio</p>
);
return (
<div>
{ isEmpty ? (emptyMsg) : (cartItems.map((item) => (
<ShoppingCartProduct
key={ item.id }
id={ item.id }
count={ count }
cartItems={ item }
handleButtonIncrease={ this.handleButtonIncrease }
/>
)))}
</div>
);
}
}

It seems like this should be ShoppingCartProduct's responsibility. If you remove this count and setCount logic from your ShoppingCart component and create it inside of the ShoppingCartProducts component, each one of the items will have their own count state that can be updated independently.
One other way of seeing this is directly mutating each cartItem, but since you didn't specify their format there's no way of knowing if they're storing any information about quantity so I would go with the first approach.

handleButtonIncrease can accept item.id as parameter so that it can update the state.cartItems.
handleButtonIncrease(itemId) {
const cartItems = this.state.cartItems.map(item => {
return item.id === itemId
? {
// apply changes here for the item with itemId
}
: item
});
this.setState((prevState) => ({
cartItems,
count: prevState.count + 1,
}));
}
After that, update your callback as well:
handleButtonIncrease={ () => this.handleButtonIncrease(item.id) }

Related

Next/React - How to correctly build up state from multiple child components

I think this is easier to explain using a codesandbox link. This follows on from a previous question of mine, which may help provide more overall context. Currently, when interacting with the child elements (i.e. inputs), the state updates to {"values":{"0":{"Text1":"test"},"1":{"bool":true}}}. The issue is that if you interact with the other inputs within a Parent component, e.g. Text2 in the Parent component with id 0, it will overwrite the value already in the state, which makes it look like this {"values":{"0":{"Text2":"test"},"1":{"bool":true}}}. I want it to look like {"values":{"0":{"Text1":"test", "Text2":"test"},"1":{"bool":true}}}.
This is my try with your problem. I would like to have childIndex instead of number like you. It would be easier to work with other components later.
Here is my codesandbox
import { useEffect, useState } from "react"
import Parent from "./Parent"
const id1 = 0
const id2 = 1
interface Boo {
childIndex: number
value: {
[name: string]: string | boolean
}
}
const GrandParent: React.FC = () => {
const [values, setValues] = useState<Boo[]>([])
const valuesChange = (e: React.ChangeEvent<HTMLInputElement>, id: number) => {
console.log("change event")
const name = e.target.name
let value: any
if (name === "bool") {
value = e.target.checked
} else {
value = e.target.value
}
setValues((prev) => {
// Update new value to values state if input already there
let updateBoo = prev.find((boo) => boo.childIndex === id)
if (updateBoo) {
// Update Values
const valKeys = Object.keys(updateBoo.value)
const valIndex = valKeys.find((val) => val === name)
if (valIndex) {
updateBoo.value[valIndex] = value
} else {
updateBoo.value = { ...updateBoo.value, [name]: value }
}
} else {
// Create new if not added
updateBoo = {
childIndex: id,
value: { [name]: value }
}
}
return [...prev.filter((boo) => boo.childIndex !== id), updateBoo]
})
}
useEffect(() => {
console.log("Render", { values })
})
return (
<>
<div>{JSON.stringify({ values }, undefined, 4)}</div>
<br />
<br />
<Parent valueChange={(e) => valuesChange(e, id1)} key={id1} />
<Parent valueChange={(e) => valuesChange(e, id2)} key={id2} />
</>
)
}
export default GrandParent
The trick is you should return the previous state of the property too:
setValues((prev) => {
return {
...prev,
[id]: {
...(prev && (prev[id] as {})), // <= return the previous property state
[name]: value
}
}
})
I'm not very good at typescript but I tried my best to solve some types' errors
you can see an example below

Cannot delete multiple items on react using firestore

I am trying to delete multiple items on click of checkbox using firestore. But, onSnapshot method of firestore is causing issue with the state.
After running the code I can click on checkbox and delete the items, the items get deleted too but I get an error page, "TyperError: this.setState is not a function" in onCollectionUpdate method.
After refreshing the page I can see the items deleted.
Here's my code:
class App extends React.Component {
constructor(props) {
super(props);
this.ref = firebase.firestore().collection('laptops');
this.unsubscribe = null;
this.state = { laptops: [], checkedBoxes: [] };
this.toggleCheckbox = this.toggleCheckbox.bind(this);
this.deleteProducts = this.deleteProducts.bind(this);
}
toggleCheckbox = (e, laptop) => {
if (e.target.checked) {
let arr = this.state.checkedBoxes;
arr.push(laptop.key);
this.setState = { checkedBoxes: arr };
} else {
let items = this.state.checkedBoxes.splice(this.state.checkedBoxes.indexOf(laptop.key), 1);
this.setState = {
checkedBoxes: items
}
}
}
deleteProducts = () => {
const ids = this.state.checkedBoxes;
ids.forEach((id) => {
const delRef = firebase.firestore().collection('laptops').doc(id);
delRef.delete()
.then(() => { console.log("deleted a laptop") })
.catch(err => console.log("There is some error in updating!"));
})
}
onCollectionUpdate = (querySnapshot) => {
const laptops = [];
querySnapshot.forEach((doc) => {
const { name, price, specifications, image } = doc.data();
laptops.push({
key: doc.id,
name,
price,
specifications,
image
});
});
this.setState({ laptops });
console.log(laptops)
}
componentDidMount = () => {
this.unsubscribe = this.ref.onSnapshot(this.onCollectionUpdate);
}
getLaptops = () => {
const foundLaptops = this.state.laptops.map((laptop) => {
return (
<div key={laptop.key}>
<Container>
<Card>
<input type="checkbox" className="selectsingle" value="{laptop.key}" checked={this.state.checkedBoxes.find((p) => p.key === laptop.key)} onChange={(e) => this.toggleCheckbox(e, laptop)} />
...carddata
</Card>
</Container>
</div>
);
});
return foundLaptops;
}
render = () => {
return (
<div>
<button type="button" onClick={this.deleteProducts}>Delete Selected Product(s)</button>
<div className="row">
{this.getLaptops()}
</div>
</div>
);
}
}
export default App;
In the toggleCheckbox function you set the this.setState to a object.
You will need to replace that with this.setState({ checkedBoxes: items})
So you use the function instead of setting it to a object
You probably just forgot to bind the onCollectionUpdate so this referes not where you expectit to refer to.
Can you pls also change the this.setState bug you have there as #David mentioned also:
toggleCheckbox = (e, laptop) => {
if (e.target.checked) {
let arr = this.state.checkedBoxes;
arr.push(laptop.key);
this.setState({ checkedBoxes: arr });
} else {
let items = this.state.checkedBoxes.splice(this.state.checkedBoxes.indexOf(laptop.key), 1);
this.setState({
checkedBoxes: items
})
}
}
If you already did that pls update your question with the latest code.

How to update object using index value in Reactjs?

I am having a onChange function i was trying to update the array options by index wise and i had passed the index to the function.
Suppose if i am updating the options array index 0 value so only that value should be update rest should remain as it is.
Demo
Here is what i tried:
import React from "react";
import "./styles.css";
export default function App() {
const x = {
LEVEL: {
Type: "LINEN",
options: [
{
Order: 1,
orderStatus: "INFO",
orderValue: "5"
},
{
Order: 2,
orderStatus: "INPROGRESS",
orderValue: "5"
},
{
Order: 3,
orderStatus: "ACTIVE",
orderValue: "9"
}
],
details: "2020 N/w UA",
OrderType: "Axes"
},
State: "Inprogress"
};
const [postdata, setPostData] = React.useState(x);
const handleOptionInputChange = (event, idx) => {
setPostData({
...postdata,
LEVEL: {
...postdata.LEVEL.options,
[event.target.name]: event.target.value
}
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e, idx)}
/>
);
})}
</div>
);
}
Suppose if i want to add the objects in another useState variable for all the updated options only, will this work?
const posting = {
"optionUpdates": [],
}
const [sentdata , setSentData] = useState(posting);
setSentData({
...sentdata,
optionUpdates: [{
...sentdata.optionUpdates,
displayOrder: event.target.value
}]
})
Basically, you need to spread properly, use callback approach to set state etc.
Change your handler to like this.
Working demo
const handleOptionInputChange = (event, idx) => {
const target = event.target; // with callback approach of state, you can't use event inside callback, so first extract the target from event.
setPostData(prev => ({ // prev state
...prev, // spread prev state
LEVEL: { //update Level object
...prev.LEVEL,
options: prev.LEVEL.options.map((item, id) => { // you need to loop thru options and find the one which you need to update.
if (id === idx) {
return { ...item, [target.name]: target.value }; //spread all values and update only orderStatus
}
return item;
})
}
}));
};
Edit Added some comments to code and providing some explanation.
You were spreading postdata.LEVEL.options for LEVEL which is incorrect. For nested object you need to spread each level.
Apparently, your event.target.name is "orderStatus", so it will add an "orderStatus" key to your postData.
You might want to do something like this:
const handleOptionInputChange = (value, idx) => {
setPostData(oldValue => {
const options = oldValue.LEVEL.options;
options[idx].orderStatus = value;
return {
...oldValue,
LEVEL: {
...oldValue.LEVEL,
options
}
};
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e.target.value, idx)}
/>
);
})}
</div>
);
See this demo

Reactjs show hide multiple components

Newbie React question here on show hide functionality.
I have a state of 'show' that I set to false:
this.state = {
show: false,
};
Then I use the following function to toggle
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
And my display is
{this.state.show && <xxxxxx> }
This all works fine. However I want to apply the function it to multiple cases (similar to accordion, without the closing of other children. So I change my constructor to
this.state = {
show: [false,false,false,false,false,false]
};
and this to recognise there are 6 different 'shows'.
{this.state.show[0] && <xxxxxx> }
{this.state.show[1] && <xxxxxx> } etc
But where I get stuck is how to account for them in my toggleDiv function. How do I insert the square bracket reference to the index of show (if this is my problem)?
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
Thanks for looking.
First of all I'd suggest you not to rely on current state in setState function, but to use the callback option to be 100% sure that you are addressing to the newest state:
this.setState((prevState) => ({ show: !prevState.show }));
How to deal with multiple elements?
You'll have to pass the index of currently clicked element.
{yourElements.map((elem, i) => <YourElem onClick={this.toggleDiv(i)} />)}
and then inside your toggleDiv function:
toggleDiv = (i) => () => {
this.setState((prevState) => {
const r = [...prevState.show]; // create a copy to avoid state mutation
r[i] = !prevState.show[i];
return {
show: r,
}
}
}
Use an array instead of a single value. In your toggle div function make a copy of the state array make necessary changes and push the entire array back up to state at the end.
This is some simplified code showing the workflow I described above
export default class myClass extends React.Component{
constructor(props){
super(props);
this.state = { show: new Array(2).fill(false) };
}
//you need a index or id to use this method
toggleDiv = (index) => {
var clone = Object.assign( {}, this.state.show ); //ES6 Clones Object
switch(clone[index]){
case false:
clone[index] = true
break;
case true:
clone[index] = false
break;
}
this.setState({ show: clone });
}
render(){
return(
<div>
{ this.state.show[0] && <div> First Div </div> }
{ this.state.show[1] && <div> Second Div </div> }
{ this.state.show[2] && <div> Third Div </div> }
</div>
)
}
}

how to render object array in react?

here is my componentDidmount
componentDidMount() {
for ( var i in course ) {
let title = course[i];
const ref = firestore.collection('courses');
ref.where("class", "array-contains", course[i]).get()
.then(querySnapshot => {
const count = querySnapshot.size
course_stats.push({
title: title,
value: count,
});
});
}
console.log(course_stats)
this.setState({
courses: course_stats,
})
}
here is my render
render() {
const { classes } = this.props;
if (this.state.courses) {
console.log(this.state.courses)
return (
<ul>
{course_stats.map(d => <li key={d.title}>{d.title}</li>)}
</ul>
)
}
on the line console.log, I can see the object array in that. However, when i try render that, it doesn't show anything.
this is the console.log capture
how can I render the title and value of array?
Thank you!
Adding to izb's answer, this.setState has already executed, so you should use async/await, or add a seperate callback function like this that returns a Promise
setAsync(state) {
return new Promise((resolve) => {
this.setState(state, resolve)
});
}
handleChange = (event) => {
return this.setAsync({[event.target.name]: event.target.value})
}

Resources