Weird destructuring behavior in React - javascript

I have a deeply nested JSON object as state initially made with useState hook -
const [template, setTemplate] = useState([
{
statement: 'this is top level statement',
nestingLevel: 0,
children: [
{
nestingLevel: 1,
statement:
'this is a statement with such a template',
children: [
{
statement: 'first chart',
nestingLevel: 2,
},
{ statement: 'second chart',
nestingLevel: 2,
},
],
},
],
},
{
statement:
'this is second statement for section with such a metric {{}}',
nestingLevel: 0,
},
]);
I have an input element with an onChange handler.
As you can see, whenever there is some change in the input text, I update some relevant key-value pair based on path. I do so by using the lodash library's get and set functions.
const handleDataChange = (e, path) => {
console.log('handling the data');
// copy the template
let templateCopy = template;
// create the new object with updated information
const tempObj = _.set(
templateCopy,
`${path}['statement']`,
e.target.value,
);
setTemplate([...tempObj]);
};
The problem is in the handleDataChange function. When I do setTemplate(tempObj) the state doesn't get updated. However when I do setTemplate([...tempObj])(which will essentially yield the same result), this later solution works as expected.
I want to know why that is the case. Is it because lodash gives results always as object and destructuring and repacking it make it array again and hence it works as expected?

The object reference stays the same when you mutate nested property only, and as react does shallow comparison in order to detect changes, it won't react to the change.
You can deepClone the object, then mutate it as you do with set and then update the state.

Related

update data with useState in React js

I am new to react. I have faced one issue and not able to solve it. I am looking for your help.
I have an array which I have listed below. All data are looped and displayed in the view. From my current array, I want to update the count of dietry array[] which is inside the fruits array.
This is my useState
const [foods, setFood] = useState(fruits)
if I console.log(foods) it gives data as below.
fruits: [
{
id: 1,
name: 'Banana',
family: 'abc',
price: 2.99,
isEnabled: true,
dietary: [
{
id:1,
disabled: false,
group: null,
selected: false,
text: 'N/A',
value: '858090000',
count:0
},
{
id:2,
disabled: true,
group: null,
selected: true,
text: 'N/A',
value: '80000',
count:0
},
}
This data are looped in a view page and there is onClick handleIncrement method which should increment the count of dietary array of index 0 or index1 etc whichever index sent from handleIncremnt() method.
This is my method
const handleIncrementCount = (dietary_index) => {
setFood(foods =>
foods.map((food,index) =>
dietary_index === food[index] ? {...food, qty:food.count+1}:food
)
);
}
I am not able to increase the count, in my view page counts are 0 even if i click the increment button.It shows some error within map
Any help is highly appreciated
I ma looking for a solutions
There are a few issues with your handleIncrementCount function. Firstly, you are trying to use the dietary_id parameter to find the correct food object to update, but you are using it to access the index property of the food object instead. This will always return undefined, and so the function will not be able to find the correct object to update.
Secondly, you are trying to update the qty property of the food object, but this property does not exist in the food object. Instead, you need to update the count property of the correct dietary object inside the food object.
Here is how you can fix these issues:
const handleIncrementCount = (dietary_id) => {
setFood(foods =>
foods.map(food => {
// Find the dietary object with the correct id
const dietary = food.dietary.find(d => d.id === dietary_id);
// If the dietary object was found, increment its count
if (dietary) {
dietary.count += 1;
}
// Return the updated food object
return food;
})
);
};
With this change, the handleIncrementCount function should be able to find the correct food object and update the count property of the correct dietary object.
Note that this function is using the Array.prototype.map() and Array.prototype.find() methods to transform the foods array and find the correct dietary object to update. These methods are commonly used in JavaScript to transform and find elements in arrays. It is worth reading up on these methods and understanding how they work in order to better understand this code.

useState updating state child array object not working

I have this state in my React component-->
const [employeeState, setEmployee] = useState({
"empName": null,
"Age": null,
"depts": [], //depts is an array of objects
"groups": [] //group is an array of objects
});
I have a separate state for depts and groups like follows -->
const depts = {
"name": "some name",
"code": "s123",
}
const [deptState, setDeptState] = useState([
{ ...depts},
]);
same is for groups too....
Now when I am trying to set the employee state on some btn click like below it's not updating and keeping depts and groups property as it is empty -->
const deptsLst = [...deptState];
const groupsLst = [...groupstate];
console.log(depsLst); // this will print the results as expected
console.log(groupsLst); // this will print the results as expected
setEmployee({
...employeeState,
['depts']: deptsLst ,
['groups']: groupsLst ,
})
console.log(employeeState);// this will show depts and groups as empty only
I am new to this spread variable copying concept. What am I doing wrong here?
You state that "I am new to this spread variable copying concept"...not only are you new to the spread operation but your code demonstrates that you do not have a basic understanding of it I think. You are using spread operators on top of spread operators on top of computed property names, all for no reason.
You are also using React state to manage the employee object and then using React state to manage Array properties on the employee object. If you are already performing state management on the employee object, why maintain state of individual properties?
Ok a couple breakdowns of your code:
const depts = {
"name": "some name",
"code": "s123",
}
const [deptState, setDeptState] = useState([
{ ...depts},
]);
Above you clone an object for no reason.
With React state management you only need to clone objects to trigger a render event. A clone creates a new object with a new reference in memory, will "react" to object reference changes and trigger a state change event. React is not capable of watching every property of a complex object, especially array elements to see if a value has changed.
The below code will suffice, you are initializing state, you do not need to clone:
const [deptState, setDeptState] = useState([{
"name": "some name",
"code": "s123",
}]);
Next this code:
setEmployee({
...employeeState,
['depts']: deptsLst ,
['groups']: groupsLst ,
})
Here you are attempting to use the spread operator to clone the "employeeState" object and update new values for the depts property key and groups property key. You use "Computed Property Names' for property keys for no reason.
This is sufficient:
setEmployee({
...employeeState,
depts: deptsLst ,
groups: groupsLst
})
You also create unnecessary state:
const [deptState, setDeptState] = //..rest of code omitted
Here you do not realize React state changes happen async:
setEmployee({
...employeeState,
['depts']: deptsLst ,
['groups']: groupsLst ,
})
console.log(employeeState);
The above code actually has changed the employeeState, just not be the time of your console.log(employeeState);
Ok, so to start I have created a CodePen for you to understand how state changes.
When you want to change state for this employee object of yours you simply need to change the object property values or add new property values and then clone the employee object to change its reference and than call setState with the clone, consider this object state:
const [employee, setEmployee] = React.useState({
"empName": 'Jane Doe',
"Age": 33,
"depts": [
{
"name": "some name",
"code": "s123",
}
]
});
Here I am adding a department to the employee, note I only clone the employee object to trigger React to detect the overall object reference change, React does not know I changed property depts array but the new state does have my new value I pushed on the department array:
function exampleUpdatingEmployee() {
employee.depts.push({
"name": "some new dept",
"code": "876",
});
setEmployee({ ...employee });
}
You can do:
setEmployee({
...employeeState,
depts: deptsLst,
groups: groupsLst
})
to update your employeeState. You probably won't see the update with the console.log you have right after that since it will execute before the state updates. You can verify that the employeeState updated by using a useEffect function like this:
useEffect(() => {
console.log(employeeState)
}, [employeeState])
The spread operator is the ES6 way of copying arrays without passing a reference to your copy so you can modify one array without those changes showing up in the other array. You can read into that more here: Reference And Copying Objects & Arrays.
if depts and group are arrays of objects, you should use spread operator in this way to copy values:
setEmployee({
...employeeState,
depts: [...deptState],
groups: [...groupstate]
});
so if:
const employeeState = {status: 'hungry'};
const deptState = ['one', 'two'];
const groupstate = ['y', 'k', 'o'];
then the first code, after spread, is like:
setEmployee({
status: 'hungry',
depts: ['one', 'two'],
groups: ['y', 'k', 'o']
});

Why is this working: updating array element in state with spread operator

When reading React Cookbook I've stumbled upon a code snippet, this function gets called when user checks a task as completed in a TODO list:
markAsCompleted = id => {
// Finding the task by id...
const foundTask = this.state.items.find(task => task.id === id);
// Updating the completed status...
foundTask.completed = true;
// Updating the state with the new updated task...
this.setState({
items: [
...this.state.items,
...foundTask
]
});
}
UPD: Somehow I've completely missed the spread operator on foundTask. So what's really happening is the state gets updated with only ...this.state.items (which was mutated), and the ...foundTask part does not go into state, since it's not a valid spread.
At first it looked like it should add a new element to the items array, instead of updating, so I went to the JS console to check:
state = { items: [{id: '0', done: false}, {id: '1', done: false}] }
upd = state.items.find(obj => obj.id === '0') // {id: "0", done: false}
upd.done = true // also updates object inside the array
state = { items: [...state.items, upd] }
/* Indeed, adds a new element:
items: Array(3)
0: {id: "0", done: true}
1: {id: "1", done: false}
2: {id: "0", done: true}
*/
So then I've downloaded the code and ran it locally. And, to my surprise, it worked! State was getting updated without any trouble, no extra elements appeared. I used React DevTools to see the live state while testing.
I searched the web, but couldn't find any examples like in the book, but with a better explanation. Usually all solutions involve using .map() to build a new array, and then replace an existing one (e.g. https://stackoverflow.com/a/44524507/10304479).
The only difference I see between the book code snippet and console test is that React is using .setState(), so maybe this helps somehow. Can anyone help to clarify, why is it working?
Thanks!
Array.find will return the first value matched in the array. Here the array consist of objects and the value returned will be the reference to the object.
const foundTask = this.state.items.find(task => task.id === id);
Here foundTask will have reference to the same object contained in the state.items. So when you modify foundTask you're modifying the same object as in state.items.
For example,
If this.state.items is [{ id: 1 }] and if you do
const foundTask = this.state.items.find(obj => obj.id === 1);
foundTask.id = 2;
console.log(this.state.items); // [{ id:2 }]
In the code,
this.setState({
items: [
...this.state.items,
...foundTask
]
});
This will update the state with updated completed value for the task.
...foundTask will give you an error in the console since foundTask will be an object and you're spreading it in an array.
Here not having ...foundTask will produce same result. Perhaps not with an error.

Delete a json inside another in React

Imagine that I have this kind of JSON object on my react state:
this.state={
parent:{
childs:[
child1:{
},
child2:{
},
child3:null,
(...)
]
}
}
to delete the child1 I did the following method:
deleteChild1 = (index,test) => {
const childs= [...this.state.parent.childs];
childs[index] = {
...childs[index],
child1: null
}
this.setState(prevState => ({
...prevState,
parent: {
...prevState.parent,
childs: [
...childs,
]
}
}))
}
This works with no problem, but imagine that I have 100 childs, I have to do 100 methods like this but instead putting the child1 to null I have to put the child100, child99, you get the idea.
My question is that is another way to put the variable null.
Thanks!
Currently your data structure isn't valid so its hard to write up a solution. If I try to create that exact state object it raises an exception Uncaught SyntaxError: Unexpected token : This is because you have an array written like an object. So first thing to do is adjust your state model to be valid syntax
this.state = {
parent: {
childs: {
child1: {
},
child2: {
},
child3: null,
...
}
}
}
Now, what you are describing / what you want to do is a dynamic key reference.
You can do this like so
const childTarget = 'child1'
childs = {
...childs,
[childTarget]: null
}
so to abstract that concept a little more. make it a function parameter
deleteChild = (childTarget) => {
then when you want to remove any particular child you can let them pass their value to this function
<Child onRemove={this.deleteChild} name={name} />
// name here would be like 'child1'
// assuming you are looping over state and rendering a child component for each item
and when the child calls this function
this.props.onRemove(this.props.name)
The answer is very simple, this would be my approach.
Create a function which updates your state with the expect result (removing that property).
If you wish to assign null that you can replace .filter() with a .map() solution. Typically if you are removing a piece of data it does not make sense to null it, but to remove it.
FYI your property childs is an array you have an object within, so you need a list of objects to fix this.
E.g.
[
{
name: "child1"
},
{
name: "child2"
},
{
name: "child3"
}
]
removeChild = (child) => {
const newChildList = this.state.parent.childs.filter(({name}) => name !== child);
this.setState(previousState => ({
...previousState,
parent: {
...previousState.parent,
childs: newChildList
}
}));
}
The key part here is that the data is being updated and overriding the original array. Because you have nested data structure we don’t want to delete any pre-existing data (hence the spreading).
Call the function however you want and if your childs array has an object with the property called name that matches the child function argument, it will be not be present on the next re-render.
Hope this helps.

How to update the value of a single property within a state object in React.js?

So I have the following object structure:
const SamplePalette = {
id: 1,
name: "Sample Palette",
description: "this is a short description",
swatches: [
{
val: "#FF6245",
tints: ["#FFE0DB", "#FFA797"],
shades: ["#751408", "#C33F27"]
},
{
val: "#FFFDA4",
tints: ["#FFFFE1"],
shades: ["#CCCB83"]
},
{
val: "#BFE8A3",
tints: ["#E7FFD7"],
shades: ["#95B77E"]
}
]
}
Let's imagine that this object is managed by the state of my app like this:
this.state = {
currentPalette: SamplePalette,
}
My question is how would I go about updating the val property of a given swatch object in the swatches array? Or more generally - how do I only update pieces of this object?
I tried using the update helper as well as to figure out how Object.assign() works, however I've been unsuccessful and frankly can't really grasp the syntax by just looking at examples.
Also, since I'm going to be modifying this object quite a lot, should I look into maybe using Redux?
[EDIT]
I tried #maxim.sh suggestion but with no success:
this.setState(
{ currentPalette: {...this.state.currentPalette,
swatches[0].val: newValue}
})
Consider you have new new_swatches
I think the clearer way is to get array, update it and put back as:
let new_swatches = this.state.currentPalette.swatches;
new_swatches[0].val = newValue;
this.setState(
{ currentPalette:
{ ...this.state.currentPalette, swatches: new_swatches }
});
Also you have : Immutability Helpers or https://github.com/kolodny/immutability-helper
Available Commands
{$push: array} push() all the items in array on the target.
{$unshift: array} unshift() all the items in array on the target.
{$splice: array of arrays} for each item in arrays call splice() on the target with the parameters provided by the item.
{$set: any} replace the target entirely.
{$merge: object} merge the keys of object with the target.
{$apply: function} passes in the current value to the function and updates it with the new returned value.

Categories

Resources