I have a problem with handling checkbox using react, what I want is the state should reflect what condition of the checkbox, and in the end I want to have [id-1, id-2, id-3] to save to the backend. But my demo seems broken, I think I miss one condition, but I can't tell what's my problem.
https://codesandbox.io/s/kpw23v4xv
handleCheckboxChange = (device_id) => {
const upateStatOfZoneCameraMenu = () => {
this.setState({
zones: [...this.state.zones.slice(0, this.state.selectedTab), {
...this.state.zones[this.state.selectedTab],
cameras: [
...this.state.zones[this.state.selectedTab].cameras.map(
o => ({
...o,
checked: this.state.selectedCameras.find(o2 => o2 === o.device_id) || o.device_id === device_id
})
)
]
}, ...this.state.zones.slice(this.state.selectedTab + 1)]
})
}
const updatedSelectedCamera = this.state.selectedCameras.find(obj => obj === device_id)
if (!updatedSelectedCamera) {
this.setState({
selectedCameras: [...this.state.selectedCameras, device_id]
}, () => {
upateStatOfZoneCameraMenu()
})
} else {
this.setState({
selectedCameras: this.state.selectedCameras.filter(obj => obj !== device_id)
}, () => {
upateStatOfZoneCameraMenu()
})
}
}
I think the problem is at line 52.
The problem with your logic was in setting the checked state of your camera.
I have changed the existing code to:
checked: this.state.selectedCameras.find(o2 => o2 === o.device_id) !==undefined
There were also warnings in your code about keys not being present in elements you create iteratively, which I fixed in App component by adding a key to the divs rendered by the map methods.
There was another warning for changing an uncontrolled component to a controlled component, which was fixed when I introduced the checked field in the initial state of your cameras and set it to false.
The detailed code can be found here.
I have forked your sandbox to provide you a working version: https://codesandbox.io/s/k5ml50ky13
Several problems you had:
Prefer to only update your state once instead of several times
Your condition on the checked property was unclear so I rewrote in order to only verify if the current camera is checked
Basically I just changed the function handleCheckboxChange:
handleCheckboxChange = (device_id) => {
const updatedSelectedCamera = this.state.selectedCameras.find(obj => obj === device_id);
const selectedCameras = updatedSelectedCamera ? this.state.selectedCameras.filter(obj => obj !== device_id) : [...this.state.selectedCameras, device_id];
this.setState({
selectedCameras,
zones: [
...this.state.zones.slice(0, this.state.selectedTab),
{
...this.state.zones[this.state.selectedTab],
cameras: [
...this.state.zones[this.state.selectedTab].cameras.map(
o => ({
...o,
checked: selectedCameras.includes(o.device_id),
})
)
]
},
...this.state.zones.slice(this.state.selectedTab + 1)
],
});
}
Hope it helps.
Related
I am on the newer side of React and trying to change the state of an object in an array. Currently, I am pulling the object out of the array, changing the property in that object, then adding the new object to the state again. Problem being that it sends the object to the back of the list and reorders my checkbox inputs.
const handleChange = (e) => {
if (e.target.type === "checkbox") {
// Get the role from the current state
const roleToChange = input.roles.find(
(role) => Number(role.id) === Number(e.target.id)
);
// Change checked state to opposite of current state
const changedRole = { ...roleToChange, checked: !roleToChange.checked };
// Get every role except the one that was changed
const newRoles = input.roles.filter(
(role) => Number(role.id) !== Number(e.target.id)
);
// Update the role in the state
setInput((prevState) => {
return { ...prevState, roles: [...newRoles, changedRole] };
});
}
Can I update the object in the array in-place so this doesn't happen?
Don't .filter - .map instead, and return the changed object in case the ID matches, so the new object gets put at the same place in the new array as it was originally.
const handleChange = (e) => {
if (e.target.type !== "checkbox") {
return;
};
const newRoles = input.roles.map((role) =>
Number(role.id) !== Number(e.target.id)
? role
: { ...role, checked: !role.checked }
);
setInput((prevState) => {
return {
...prevState,
roles: newRoles
};
});
}
Unless the state is updated synchronously before this, which sounds a bit unlikely (but not impossible), you can also probably use setInput({ ...input, roles: newRules }) instead of the callback.
i have this react state in mi component
this.state =
{
Preferences: []
}
and i want to push only if the element not exists because i dont want the same repeated element, and i want to pop the element if is already exists this is the function what i use
SelectPreference = Id =>
{
if(!this.state.Preferences.includes(Id))
{
this.setState(state =>
{
const Preferences = state.Preferences.concat(Id);
return {
Preferences,
};
});
}
else
{
this.setState(state =>
{
const Preferences = state.Preferences.filter(Item => Item !== Id);
return {
Preferences,
};
});
}
console.log(this.state.Preferences);
}
the problem is when i push a object it says the array is empty and when i pop the element it says i have a element. i dont kwon how to do it
You can do as the following if you want your preferences to be uniq.
Based on your code, I understand that id is a string. Tell me if it's not the case.
For more info about Set and uniq value in array
if(!this.state.Preferences.includes(Id)) {
this.setState(state => ({
// Using a set will make sure your id is uniq
Preferences: [...new Set([...state.Preferences, id])]
}))
}
else {
this.setState(state => ({
// You're code was good, I only change syntax
Preferences: state.Preferences.filter(Item => Item !== Id)
}))
}
setState actions are asynchronous and are batched for performance gains. It will be pretty hard to immediately check the updated this.state.Preferences in the same function block.
Still if you need to check the updated value of the Preferences, you can provide a callback which will be executed immediately after change in state.
this.setState({ Preferences }, () => console.log(this.state))
Also, there is simpler version of your logic.
if(this.state.Preferences.includes(Id)) {
this.setState({
Preferences: this.state.Preferences.filter(id => id !== Id);
});
} else {
this.setState({
Preferences: this.state.Preferences.concat(Id);
});
}
Goal :
Within a React App, understand how to correctly update a boolean value that is stored within an array in state.
Question :
I am not sure if my code is correct in order to avoid asynchronous errors (whether or not it works in this case I am not interested in as my goal is to understand how to avoid those errors in all cases). Is there some syntax that is missing in order to correctly create a new state?
Context :
Creating a todo list within React.
My state consists of an array labeled itemsArr with each array element being an object
The objects initialize with the following format :
{ complete : false, item : "user entered string", id : uuid(), }
Upon clicking a line item, I am calling a function handleComplete in order to strikethrough the text of that line by toggling complete : false/true
The function is updating state via this :
handleComplete(id){
this.setState(state =>
state.itemsArr.map(obj => obj.id === id ? obj.complete = !obj.complete : '')
)
}
Additional Details :
One of my attempts (does not work) :
handleComplete(id){
const newItemsArr = this.state.itemsArr.map(obj =>
obj.id === id ? obj.complete = !obj.complete : obj);
this.setState({ itemsArr : newItemsArr })
}
In both snippets you haven't correctly returned a new object from the .map callback:
handleComplete(id){
const newItemsArr = this.state.itemsArr.map(obj =>
obj.id === id ? { id: obj.id, complete: !obj.complete } : obj);
this.setState({ itemsArr : newItemsArr });
}
Your function, as mentioned above, returns and updates wrong data, by adding new elements in your state instead of updating your current array.
Since array.map() returns an array, you can assign it to the state array, itemsArr.
You should also replace the change condition by updating the element's value first, and then returning it, if its id matches,or else, simply leave it as is.
handleComplete=(id)=>{
this.setState(state =>{
itemsArr:state.itemsArr.map(obj => obj.id === id
? (obj.complete = !obj.complete,obj) //update element's complete status and then return it
: obj) //return element as is
},()=>console.log(this.state.itemsArr)) //check new state
}
live example using your data : https://codepen.io/Kalovelo/pen/KKwyMGe
Hope it helps!
state = {
itemsArr: [
{
complete: false,
item: 'iemName',
id: 1
},
{
complete: false,
item: 'iemName',
id: 2
}
]
}
handleComplete = (id) => {
let { itemsArr } = { ...this.state };
itemIndex = itemsArr.findIndex(item => id === item.id);
itemsArr[itemIndex]['complete'] = !itemsArr[itemIndex]['complete'];
this.setState{itemsArr}
}
I have this code in my constructor:
this.state = {
tests: [
{
question: "1",
answer: "2",
user: ""
},
{
question: "1",
answer: "2",
user: ""
},
],
};
I have edit function where I read event value in my input:
edit(id, event) {
this.state.tests[id].user = event.target.value;
this.setState({tests:this.state.tests});
}
But es give me this warning:
Do not mutate state directly. Use setState()
react/no-direct-mutation-state
What can i do in this case? Maybe somehow change the line with the assignment event.target.value into setState()?
You can use map() to create copy of tests
edit(id, event) {
const user = event.target.value;
const tests = this.state.tests.map((x,i) => i === id ? {...x, user} : x);
this.setState({tests});
}
One way I tend to go is to make a copy of the array first and then change an item in it, or change the array itself, and then set the state
var tests = this.state.tests.slice(0);
tests[id].user = event.target.value;
this.setState({tests:tests});
You may want to deep-clone the array in some cases, sometimes not.
You are correct, that the problem is with the line:
this.state.tests[id].user = event.target.value;
That's the point where you are mutating your state directly.
You have a few options.
You can "clone" the array first and then update it:
const newTests = [...this.state.tests];
newTests[id].user = event.target.value;
this.setState({tests: newTests});
You could also use immutability-helper:
const {value} = event.target;
this.setState(prevState => update(prevState, {[id]: {user: {$set: value}}}));
In this example, you need to extract value from your event, because it's not safe to access event values in asynchronous calls after an event has been handled.
edit(id, event) {
var newNote = {...this.state.tests[id]}
newNote.user = event.target.value
this.setState({ tests: [...this.state.tests, newNote]})
}
First of all, when you try to set a the new state using the data from previous state you have to use the updater as a function
https://reactjs.org/docs/react-component.html#setstate
const edit = (id, event) => {
this.setState((prevState) => {
const tests = [...prevState.tests];
tests[id] = {
...tests[id],
user: event.target.value
};
return {
...prevState,
tests
};
});
};
When state is not heavy, I use the following codes:
edit (id, event) {
const cp = JSON.parse(JSON.stringify(this.state.tests))
cp[id].user = event.target.value
this.setState({ tests: cp })
}
Update: I found Immer solves it perfectly.
import produce from 'immer'
edit (id, event) {
this.setState(
produce(draft => draft.tests[id].user = event.target.value)
)
}
I am trying to implement a queue wherein the user can switch items from one list to another. i.e. from "available" to "with client", where the state of the queue is held in the root React component like so:
this.state = {
queue: {
available: [{ name: "example", id: 1 }, ...],
withClient: [],
unavailable: []
}
};
However my move function is broken:
move(id, from, to) {
let newQueue = deepCopy(this.state.queue);
console.log("NEW QUEUE FROM MOVE", newQueue); // { [from]: [], [to]: [undefined] }
console.log("QUEUE IN STATE FROM MOVE", this.state.queue); // { [from]: [{...}], [to]: [] }
let temp = newQueue[from].find(x => x.id === id);
newQueue[from] = this.state.queue[from].filter(x =>
x.id !== id
);
newQueue[to] = this.state.queue[to].concat(temp);
this.setState({
queue: newQueue
});
}
I am expecting the 2 console.logs to be the same. There seems to be some sort of race condition happening here that I am not understanding. It results in getting an error Cannot read property 'id' of undefined
Right now the only way the movement is triggered from a HelpedButton component contained within each item in the "Available" list which gets passed props:
class HelpedButton extends React.Component {
constructor() {
super();
this.clickWrapper = this.clickWrapper.bind(this);
}
clickWrapper() {
console.log("I have id right?", this.props.id); //outputs as expected
this.props.move(this.props.id, "available", "withClient");
}
render() {
return (
<span style={this.props.style}>
<button onClick={this.clickWrapper}>
<strong>Helped A Customer</strong>
</button>
</span>
);
}
}
export default HelpedButton;
I don't think there's anything wrong with the deepCopy function, but here it is, imported file from node_modules:
"use strict";
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
module.exports = deepCopy;
The recommended way by react documentation to make setState which depends on previous state is to use the updater form which looks like this setState((prevState,prevProp)=>{}) . With this method your move function will look like this.
move(id, from, to) {
let temp = newQueue[from].find(x => x === x.id);
newQueue[from] = this.state.queue[from].filter(x =>
x.id !== id
);
newQueue[to] = this.state.queue[to].concat(temp);
this.setState((prevState)=>{
queue: {
...prevState.queue,
[from]: prevState.queue[from](o => x.id !== id),
[to]: [from]: prevState.queue[from].concat(prevState.queue[from].find(x => x === x.id))
}
});
}
I believe the reason why you see different outputs is console.log outputs a live object which means if you run console.log(obj) and later change obj param the displayed params are changes. Try to debug with console.log("obj: " + JSON.strignify(obj)).
Here is more about why you should call the async method of setState when relying on previous state in react docs