React useState behaviour , questions on the need of a spread (....) operator - javascript

why cant I return expenses1 without spreading it in an array? since expenses1 which is the previous data is an array containing an object? from what i can see, {listOfExpenses} takes in an array of objects which is === expenses1
import Expenses from "./components/Expenses/Expenses";
import NewExpenses from "./components/NewExpenses/NewExpenses";
import React, { useState } from "react";
const expenses = [
{ id: "e1", title: "Toilet Paper", amount: 94.12, date: new Date(2020, 7, 14),},
{ id: "e2", title: "New TV", amount: 799.49, date: new Date(2021, 2, 12), },
{ id: "e3", title: "Car Insurance", amount: 294.67, date: new Date(2021, 2, 28),},
{ id: "e4", title: "New Desk (Wooden)", amount: 450, date: new Date(2021, 5, 12), },
];
function App() {
const [listOfExpenses, setListOfExpenses] = useState(expenses);
const addExpenseHandler = (expenseData) => {
setListOfExpenses((expenses1) => {
console.log(expenses1);
expenses1.push(expenseData);
console.log(...expenses1);
return [...expenses1];
});
};
return (
<div>
<NewExpenses onAddExpense={addExpenseHandler} />
<Expenses expenses={listOfExpenses} />
</div>
);
}
So I've tried console logging and compare the data difference between expenses1 and listOfExpenses, they are both the same data type thus why the need of spreading in an array?

You should never mutate data structures in React (or Redux or RxJs etc.)
The .push() method will mutate the existing array. This means it will update all references to that array, including the one internally uses to keep track of what needs to be updated. Since the old array and new array are equal, react won't update.
Instead, you should always make a new array. While the spread operator, which will copy the array will technically work, it's only because the equality operator in JS is not very good. Do do it properly use:
setListOfExpenses(expenses=>[...expenses, expenseData])
This will create a new array that starts with the original expenses, then adds the expenseData to the end. This will never mutate the original array, so properly update in all cases, even for libraries that use proper equality checks.

why cant i "return expenses1" without spreading it in an array
Because the first rule of React state is do not modify state directly, which includes not modifying the state of objects that are held in state (like arrays), see the documentation. If you don't return a different array than the one in state, React doesn't see a difference, and doesn't necessarily re-render properly.
The code you have that is using spread is still technically incorrect (though largely harmless I suspect), it should be copying the array before adding to it (or at the same time), not adding it to the array in state (which mutates it, which you mustn't do). Creating a new array by copying a previous one and adding an element can be done with a single array literal:
setListOfExpenses((expenses) => {
return [...expenses, expenseData];
});
or even
setListOfExpenses((expenses) => [...expenses, expenseData]);

Related

Weird destructuring behavior in React

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.

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

How is it possible that dynamic components overwrite each other's states?

I have created dynamic Room components which are created based on a room object dispatched by Redux.
{
rooms && rooms.map((room, index) => {
const { name, temperature, humidity, timestamp } = room
return (
<Col key={index} xs={12} md={6}>
<Room
index={index}
name={name}
temperature={temperature}
humidity={humidity}
/>
</Col>
)
})
}
The details of each room are mounted properly. I create a function to maintain 10 objects in an array. However, when the array is passed into Rechart, it seems that my components are updating on top of the same state.
class Room extends Component {
linechart = () => {
const { timestamp, temperature, humidity, name } = this.props
const { chartData } = this.state
if(chartData.length > 9) chartData.shift()
chartData.push({
name,
timestamp: moment(timestamp).format('mm:ss'),
temperature,
humidity})
}
}
As you can see, the component details are displayed properly. However, the values of chartData are being stored in the same state, despite being unique components.
I ran the function with an interval of 1 second, and the logs show that the state is being updated in 0.5 second intervals. That means both <Room/> components are using the same <LineChart/> component.
Does anyone know how to overcome this issue?
In order to update items in an array, I'd recommend using the spread syntax. Spread syntax works by shallow copying the array, which keeps a reference to the original array, but allows you to overwrite any data types stored within it. This keeps the array immutable.
I'm not sure where you're getting your data from, but since you have a finite amount of rooms, then the array should already be structured like so:
data: [
{
name: "Room1",
humidity: 11,
tempature: 30,
timestamp: "Sat May 25 2019 22:23:06 GMT-0700",
},
{
name: "Room2",
humidity: 11,
tempature: 25,
timestamp: "Sat May 25 2019 22:23:06 GMT-0700",
},
...etc
]
Then you can simply store this to state and when needed map over the array and update its properties with incoming data. However, this assumes incoming data contains all the rooms. If you just need to update a specific room within the rooms array, then you can compare an id (or something that uniquely identifies it) to an incoming new data id.
For example, a new update comes in, but it's only updating Room1, then we could do something like so..
incoming data
data: [
{
name: "Room1",
humidity: 11,
tempature: 30,
timestamp: "Sat May 25 2019 22:23:06 GMT-0700",
}
];
current data stored in state as "rooms"
this.setState(prevState => ({
...prevState, // spread out any previous state not related to "rooms"
rooms: prevState.rooms.map(room => { // map over "rooms" and pull out each "room" object
return room.name === data[0].name // if the "room.name" matches incoming data's "name", then...
? { ...data[0] } // spread and overwrite it with incoming data
: room; // else leave the "room" as is
})
}));
Using array prototypes like pop and shift mutate the original array stored within state and React doesn't handle mutations to its state. You can, however, clone the room array with Array.from() or simply create a new array instance and mutate this new instance with some array prototype functions, but you must then re-set this new instance array to state to overwrite the old array -- React handles this without a problem. It's just not as clean as the spread syntax.
Working example includes both spread syntax and Array.prototype options with a setInterval:
Working example of randomly updating one item within an array by an id with a setInterval:

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.

Replace object in array on react state

This question might fall a little on the side of a "best practice" question, but please bear with me.
Here is a portion of my state:
this.state = {
typeElements: {
headers: [
{
name: "h1",
size: 70,
lineHeight: 1.25,
kearning: 0,
marginAfter: 0
}, {
name: "h2",
size: 70,
lineHeight: 1.25,
kearning: 0,
marginAfter: 0
}, {
name: "h3",
size: 70,
lineHeight: 1.25,
kearning: 0,
marginAfter: 0
}...
What I need to do is REPLACE the object at a given index on the headers array.
I don't know how to do that with the setState method as in this.setState(headers[1] = {obj}) - but that's obviously invalid. My current method is creating a new array and clobbering the old one like this:
_updateStyle(props) {
let newState = Object.assign({}, this.state)
newState.typeElements.headers[props.index] = props
this.setState(newState)
};
For my small hacky project I guess it's OK but I feel like this is super heavy handed and would quickly lead to performance issues at any kind of scale.
Updated: since this answer still gets upvotes, be aware that the previous answer below is outdated with modern JavaScript and React. The "update" addon is now legacy and "immutability-helper" can be used instead.
The React docs also mention why immutability is important so avoid mutating state. For immutable updates you can use Object.assign() or spread syntax which needs to be done for every level of nesting, like in this example the nested headers object and its array elements. In this particular example we can use the array index as key so it's possible to also use the spread operator to make a shallow clone of the array and assign a new object as value at given index in the cloned array.
_updateStyle (props) {
const { typeElements } = this.state;
const updatedHeaders = [...typeElements.headers];
updatedHeaders[props.index] = props;
this.setState({
...this.state,
typeElements: {
...typeElements,
headers: updatedHeaders
}
));
}
Another solution which doesn't require the spread syntax and is needed if we are not using the array index to find the object we want to replace, is using array.map to create a new array and returning the new object instead of the old one at given index.
const updatedHeaders = typeElements.headers.map((obj, index) => {
return index === props.index ? props : obj;
});
Similar examples in the Redux docs also explain "immutable update patterns".
React has some immutability helpers for this, which is explained in
the docs: https://facebook.github.io/react/docs/update.html
In your case you could use the $splice command to remove one item and
add the new one at given index, for example:
_updateStyle (props) {
this.setState(update(this.state.typeElements,
{ $splice: [[props.index, 1, props]] }
));
}
Offering a better explanation of how to accomplish this.
First, find the index of the element you're replacing in the state array.
Second, update the element at that index
Third, call setState with the new collection
import update from 'immutability-helper';
// this.state = { employees: [{id: 1, name: 'Obama'}, {id: 2, name: 'Trump'}] }
updateEmployee(employee) {
const index = this.state.employees.findIndex((emp) => emp.id === employee.id);
const updatedEmployees = update(this.state.employees, {$splice: [[index, 1, employee]]}); // array.splice(start, deleteCount, item1)
this.setState({employees: updatedEmployees});
}
use immutability-helper
you can find nice examples there
Object.Assign uses shallow copy, not deep copy.
Please be aware that in your example Object.assign({}, this.state) copies only links to the nearest children of the state, i.e. to typeElements, but headers array is not copied.
There is a syntactic sugar ... in ES6 for Object.Assign, Babel has the special addon transform-object-rest-spread.
let newHeaders = { ...this.state.typeElements.headers};
newHeaders[index] = props.Something;
let newState = {...state, typeElements: { newHeaders }};

Categories

Resources