Push and pop on react state - javascript

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);
});
}

Related

Updating an object property within an array of objects in React

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.

Why is my globalState state object being updated before I update it myself?

I have a multitabbed view that I am controlling the data with through a global state, being passed through useContext (along with the setState updater function).
The structure is similar to
globalState: {
company: {
list: [
[{name: ..., ...}, {field1: ..., ... }],
[{name: ..., ...}, {field1: ..., ... }],
...
]
},
...
}
I have a table in this first tab, where each row that displays the details in the first object of each inner list array (globalState.company.list[X][0]), and has a few checkboxes to toggle fields in the second object in each inner list array (globalState.company.list[X][1]).
The issue I am having is that when I check a checkbox for a specific field, all companies have that field set to that value before I call setGlobalState(...) in that onChange call from the checkbox itself.
Here is all the related code for the flow of creating the checkbox and the handler:
<td><Checkbox
disabled={tpr.disabled} // true or false
checked={tpr.checked} // true or false
onChange={checkboxOnChange} // function handler
targetId={company.id} // number
field={"field1"} />
</td>
Checkbox definition
const Checkbox = React.memo(({ disabled, checked, onChange, targetId, field }) => {
return (
<input
type="checkbox"
style={ /* omitted */ }
defaultChecked={checked}
disabled={disabled}
onChange={(e) => onChange(e, targetId, field)}
/>
);
});
onChange Handler callback
const checkboxOnChange = (e, id, field) => {
const checked = e.target.checked;
console.log("GLOBAL STATE PRE", globalState.companies.list);
let foundCompany = globalState.companies.list.find(company => company[0].id === id);
foundCompany[1][field].checked = checked;
console.log("foundCompany", foundCompany);
console.log("GLOBAL STATE POST", globalState.companies.list);
setGlobalState(prevState => ({
...prevState,
companies: {
...prevState.companies,
list: prevState.companies.list.map(company => {
console.log("company PRE ANYTHING", company);
if (company[0].id === foundCompany[0].id) {
console.log("Inside here");
return foundCompany;
}
console.log("company", company);
return company;
})
}
}));
};
I see from the GLOBAL STATE PRE log that if I were to check a box for field1, then all companies would have field1 checked well before I modify anything else. I can confirm that before the box is checked, the globalState is as I expect it to be with all of the data and fields correctly set on load.
In the picture below, I checked the box for TPR in the second company array, and before anything else happens, the second and third companies already have the TPR set to true.
Any help would be appreciated, and I will share any more details I am able to share. Thank you.
Just don't mutate the state object directly:
const checkboxOnChange = (e, id, field) => {
const checked = e.target.checked;
setGlobalState(prevState => ({
...prevState,
companies: {
...prevState.companies,
list: prevState.companies.list.map(company => {
if (company[0].id === id) {
return {
...company,
checked
};
}
return {
...company
};
})
}
}));
};
The globalState object is being updated before you call setGlobalState because you are mutating the current state (e.g. foundCompany[1][field].checked = checked;)
One way of getting around this issue is to make a copy of the state object so that it does not refer to the current state. e.g.
var cloneDeep = require('lodash.clonedeep');
...
let clonedGlobalState = cloneDeep(globalState);
let foundCompany = clonedGlobalState.companies.list.find(company => company[0].id === id);
foundCompany[1][field].checked = checked;
I recommend using a deep clone function like Lodash's cloneDeep as using the spread operator to create a copy in your instance will create a shallow copy of the objects within your list array.
Once you have cloned the state you can safely update it to your new desired state (i.e. without worry of mutating the existing globalState object) and then refer to it when calling setGlobalState.

How is React updating his state?

I have a question concerning React and how state must be updated.
Let's say we have a class Players containing in its state an array of objects called players. We want to update one player in this array. I would have done it this way:
class Players extends Component {
state = {
players: []
}
updatePlayer = id => {
const players = this.state.players.map(player => {
player.updated = player.id === id ? true:false;
return player
});
this.setState({players: players});
}
}
But my coworker just did it this way, and it's also working:
updatePlayer = id => {
const playerObj = this.state.players.find(item => {
return item.id === id
})
if (playerObj) {
playerObj.updated = true
this.setState({ playerObj })
}
}
React's function setState update the players array without telling explicitly to do it. So, I have two questions:
Is it using a reference from the find function, and using it to update the players arrays ?
Is one of those ways recommended ?
Thank you all for your explanations !
The difference is that second snippet misuses setState to trigger an update because it uses playerObj dummy property. This could be achieved with forceUpdate.
Neither of these ways are correct. Immutable state is promoted in React as a convention. Mutating existing state may result in incorrect behaviour in components that expect a state to be immutable. They mutate existing player object, and new player.update value will be used everywhere where this object is used, even if this is undesirable.
An idiomatic way to do this is to use immutable objects in state:
updatePlayer = id => {
this.setState(({ players }) => ({
players: players.map(player => ({
...player,
updated: player.id === id
}));
});
}
Notice that setState is asynchronous, updater function has to be used to avoid possible race conditions.
Yes, all it's using a reference. All javascript objects are references so whenever you do a find you get a reference to the object, so mutating it will update it.
const players = this.state.players.map(player => {
return { ...player, updated: player.id === id };
});
this.setState({players: players});
As for the recommended way, you should stick with yours where you explicitly update the state variable that you care about.
Both of them are not correct, because you are mutating state.
The best way is a create a deep copy of this array ( just clone ) and after that make some changes with this cloned array
You can also use lodash _.cloneDeep();
For example
class Example extends React.Component {
state = {
players: [
{id: 0, name: 'John'};
]
};
updatePlayer = id => {
const { players } = this.state;
const clonePlayers = players.map(a => ({...a}));
const player = clonePlayers.find(playerId => playerId === id);
player.name = 'Jack';
this.setState(() => ({
players: clonePlayers
}));
}
render() {
return (
<div>some</div>
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
well, basically they are not the same, your coworkers code is working just because he is using an old reference of the object. so, lets take a look:
updatePlayer = id => {
const players = this.state.players.map(player => {
player.updated = player.id === id ? true:false;
return player
});
this.setState({players: players});
}
on your function, you are creating a new array using your old array, which is the correct way of doing this.
updatePlayer = id => {
const playerObj = this.state.players.find(item => {
return item.id === id
})
if (playerObj) {
playerObj.updated = true
this.setState({ playerObj })
}
}
here your friend is editing the reference of the object that he got using find and then saving a playerObj which is nothing more than the reference of a player from the array that you wanted to edit. after this you should notice that the new state will be something like
this.state = {
players: [p1, p2 ,p3, p4], // each p is a player.
//notice that playerObj is now a reference to some of the P on the players array
playerObj: {
...playerstuff,
updated: true
}
}
hope it helps :)

How to delete object from array using object property - React

I have a todo list that holds a delete button in a grandchild, that when clicked fires an event in the parent - I am wanting this event to delete the array entry corresponding to the grandchild clicked.
Parent (contains the array and my attempt at the function)
const tasks = [
{ name: 'task1', isComplete: false },
{ name: 'task2', isComplete: true },
{ name: 'task3', isComplete: false },
]
// taskToDelete is the name of the task - doesn't contain an object
deleteTask(taskToDelete) {
this.state.tasks.remove(task => task.name === taskToDelete);
this.setState({ tasks: this.state.tasks });
}
Any help would be appreciated
Two issues there:
You're seeming to try to direct modify this.state.tasks. It's important not to do that, never directly modify this.state or any object on it. See "Do Not Modify State Directly" in the React documentation for state.
You're passing an object to setState that is derived from the current state. It's important never to do that, too. :-) Instead, pass setState a function and use the state object it passes you when calling that function. From "State Updates May Be Asynchronous" in the documentation:
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state... [Instead]...use a second form of setState() that accepts a function rather than an object.
(my emphasis)
I figure your remove on an array was intended to be hypothetical, but for the avoidance of doubt, arrays don't have a remove method. In this case, the best thing to do, since we need a new array, is to use filter to remove all entries that shouldn't still be there.
So:
deleteTask(taskToDelete) {
this.setState(prevState => {
const tasks = prevState.tasks.filter(task => task.name !== taskToDelete);
return { tasks };
});
}
You could simply filter the array :
this.setState(prevState => ({
tasks: prevState.tasks.filter(task => task.name !== 'taskToDelete')
}));
Also when updating based on this.state, its better to use the function form because setState is async.
You can use filter to remove one object from an array following the immutable pattern (filter will create a new array) :
deleteTask(taskToDelete) {
const newTaskArray = this.state.tasks.filter(task => task.name !== taskToDelete);
this.setState({ tasks: newTaskArray });
}
Edit : codepend of the solution : https://codepen.io/Dyo/pen/ZvPoYP
You can implement deleteTask method as below:
deleteTask(taskToDelete) {
this.setState((prevState, props) => {
const tasks = [...prevState.tasks];
const indexOfTaskToDelete = tasks.findIndex(
task => task.name === taskToDelete
);
tasks.splice(indexOfTaskToDelete, 1);
return { tasks };
});
}
A. Find the index of taskToDelete.
B. Then use splice method to delete the item from the collection
C. Then call setState to update the state with tasks.
You can use higher order function Array#filter to delete the task.
let updatedTasks = this.state.tasks.filter(task => task.name !== taskToDelete);
this.setState({ tasks: updatedTasks });
I have followed below steps to delete a particular selected Object from the state array:
Here I am using a list of checkBoxes, when I am selecting a checkBox it will add it in the state array and when it gets de-selected then it will get deleted from the array.
if (checked) {
var tempObject = { checkboxValue: data, label: title }
this.state.checkBoxState.push(resTemp);
} else {
var element = data; //data is coming from different method.
for (let index = 0; index < this.state.checkBoxState.length; index++) {
if (element === this.state.checkBoxState[index].checkboxValue) {
this.state.checkBoxState.splice(index, 1);
}
}
}
I got stuck for this question and I am sharing my solution. Hope it will help you.

binding checkbox with nested array of objects in react

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.

Categories