Using spread operator to setState in multiple succession - javascript

I have some values stored in local storage. When my component mounts, I want to load these values into the state. However, only the last property being added is added to the state. I've checked the values on my localStorage, and they are all there. Furthermore, when I log the variables (desc, pic or foo) in the condition block, they are there.
I thought at first each subsequent if block is re-writing the state, but this is not the case as I am using the spread operator correctly (I think!), adding the new property after all pre-existing properties.
I think the problem is that the code in the last if block is running before the state is set in the first if block. How do I write the code so I get all three properties from my local storage into the state?
//what I expect state to be
{
textArea: {
desc: 'some desc',
pic: 'some pic',
foo: 'some foo'
}
}
//what the state is
{
textArea: {
foo: 'some foo'
}
}
componentDidMount () {
const desc = window.localStorage.getItem('desc');
const pic = window.localStorage.getItem('pic');
const foo = window.localStorage.getItem('foo');
if (desc) {
console.log(desc) //'some desc'
this.setState({
...this.state,
textArea: {
...this.state.textArea,
desc: desc,
},
}, ()=>console.log(this.state.textArea.desc)); //undefined
}
if (pic) {
console.log(pic) //'some pic'
this.setState({
...this.state,
textArea: {
...this.state.textArea,
pic: pic,
},
}, ()=>console.log(this.state.textArea.pic)); //undefined
}
if (foo) {
console.log(foo) //'some foo'
this.setState({
...this.state,
textArea: {
...this.state.textArea,
foo: foo,
},
}, ()=>console.log(this.state.textArea.foo)); //'some foo'
}
}

You are likely being caught by React batching setState calls by shallow-merging the arguments you pass. This would result in only the last update being applied. You can fix this by only calling setState once, for example:
componentDidMount () {
const desc = window.localStorage.getItem('desc');
const pic = window.localStorage.getItem('pic');
const foo = window.localStorage.getItem('foo');
this.setState({
textArea: Object.assign({},
desc ? { desc } : {},
pic ? { pic } : {},
foo ? { foo } : {}
)
});
}
The other version is to pass an update function to setState rather than an update object, which is safe to use over multiple calls. The function is passed two arguments: the previous state, and the current props - whatever you return from the function will be set as the new state.
componentDidMount () {
const desc = window.localStorage.getItem('desc');
const pic = window.localStorage.getItem('pic');
const foo = window.localStorage.getItem('foo');
this.setState(prevState => {
if (desc) {
return {
textArea: {
...prevState.textArea,
desc
}
}
} else {
return prevState;
}
});
// Repeat for other properties
}
It's a little more verbose using this approach, but does offer the opportunity to extract state updating functions outside of your component for testability:
// Outside component
const updateSubProperty = (propertyName, spec) => prevState => {
return {
[propertyName]: {
...prevState[propertyName],
...spec
}
}
}
const filterNullProperties = obj => {
return Object.keys(obj).reduce((out, curr) => {
return obj[curr] ? { ...out, [curr]: obj[curr] } : out;
}, {});
}
componentDidMount () {
this.setState(updateSubProperty("textArea",
filterNullProperties(
desc: window.localStorage.getItem('desc'),
pic: window.localStorage.getItem('pic'),
foo: window.localStorage.getItem('foo')
)
));
}
This way adds some complexity, but (in my opinion) gives a really readable component where it is clear to our future selves what we were trying to achieve.

Related

Change Nested State Value With A Single Function? (javascript/React)

I have some react user privilege state data I need to manage. I would like the ability to change the object privileges based on their property through a dynamic function. I'm not sure how to target the specific nested privilege property to change the value. Is this possible?
Question: How can I change the value of a nested privilege property to the functions type and value parameter?
Heres an Example:
const [userPrivilages, setUserPrivilages] = useState([{
_id: "123"
privilages: {
edit: true, //before!
share: true,
del: false
}
},
{
...more users
}
])
//my attempt
const changePrivilage = (type, value) => {
const newPrivilages = userPrivilages.map(user => {
return {
...user,
privilages: {
...privilages,
//change the privilage of "type" from the functions parameter to the value parameter
}
}) setUserPrivilages(newPrivilages)
}
changePrivilage("edit", false)
Desired output:
const [userPrivilages, setUserPrivilages] = useState([{
_id: "123"
privilages: {
edit: false, //After!
share: true,
del: false
}
},
{
...more users
}
])
Thanks!
You can use [] to refer to variable as a key like below:
const changePrivilage = (type, value) => {
const newPrivilages = userPrivilages.map(user => {
return {
...user,
privilages: {
...user.privilages,
[type]: value // here it is !!!
}
}) setUserPrivilages(newPrivilages)
}
Try this :
(see comments for understanding code)
const changePrivilage = (type,value) => {
const newUserPrivilages = userPrivilages.map(user => {
let newPrivilages = user.privilages; // get old privilages of user
newPrivilages[type] = value; // update type with new value
return {
...user,
privilages: newPrivilages, // set privilages as newPrivilages
};
});
setUserPrivilages(newUserPrivilages);
};
Note : this will change properties for all users. If you want to update only for specific user, pass _id as well to changePrivilage and execute newPrivilages[type] = value; // update type with new value inside if condition comparing user _id.

Creating new array vs modifing the same array in react

Following is the piece of code which is working fine, but I have one doubt regarding - const _detail = detail; code inside a map method. Here you can see that I am iterating over an array and modifying the object and then setting it to setState().
Code Block -
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
const { invoiceData } = this.state;
invoiceData.map(invoiceItem => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(detail => {
const _detail = detail;
if (_detail.tagNumber === data.tagNumber) {
_detail.id = data.id;
}
return _detail;
});
}
return invoiceItem;
});
state.invoiceData = invoiceData;
}
this.setState(state);
};
Is this approach ok in React world or I should do something like -
const modifiedInvoiceData = invoiceData.map(invoiceItem => {
......
code
......
})
this.setState({invoiceData: modifiedInvoiceData});
What is the pros and cons of each and which scenario do I need to keep in mind while taking either of one approach ?
You cannot mutate state, instead you can do something like this:
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
this.setState({
invoiceData: this.state.invoiceData.map(
(invoiceItem) => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(
(detail) =>
detail.tagNumber === data.tagNumber
? { ...detail, id: data.id } //copy detail and set id on copy
: detail //no change, return detail
);
}
return invoiceItem;
}
),
});
}
};
Perhaps try something like this:
checkInvoiceData = (isUploaded, data) => {
// Return early
if (!isUploaded) return
const { invoiceData } = this.state;
const updatedInvoices = invoiceData.map(invoiceItem => {
if (invoiceItem.number !== data.savedNumber) return invoiceItem
const details = invoiceItem.details.map(detail => {
if (detail.tagNumber !== data.tagNumber) return detail
return { ...detail, id: data.id };
});
return { ...invoiceItem, details };
});
this.setState({ invoiceData: updatedInvoices });
};
First, I would suggest returning early rather than nesting conditionals.
Second, make sure you're not mutating state directly (eg no this.state = state).
Third, pass the part of state you want to mutate, not the whole state object, to setState.
Fourth, return a new instance of the object so the object reference updates so React can detect the change of values.
I'm not saying this is the best way to do what you want, but it should point you in a better direction.

Which approach in React is better?

Below both code does exactly same but in different way. There is an onChange event listener on an input component. In first approach I am shallow cloning the items from state then doing changes over it and once changes are done I am updating the items with clonedItems with changed property.
In second approach I didn't cloned and simply did changes on state items and then updated the state accordingly. Since directly (without setState) changing property of state doesn't call updating lifecycles in react, I feel second way is better as I am saving some overhead on cloning.
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
const clonedItems = Array.from(items);
clonedItems.map((ele: NetworkItem) => {
if (ele.nicType === type) {
ele.rate = Number(value);
}
});
this.setState({ items: clonedItems });
};
OR
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
items.map((ele: NetworkItem) => {
if (ele.nicType === type) {
ele.rate = Number(value);
}
});
this.setState({ items });
};
You can use this
this.setState(state => {
const list = state.list.map(item => item + 1);
return {
list,
};
});
if you need more info about using arrays on states, please read this: How to manage React State with Arrays
Modifying the input is generally a bad practice, however cloning in the first example is a bit of an overkill. You don't really need to clone the array to achieve immutability, how about something like that:
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
const processedItems = items.map((ele: NetworkItem) => {
if (ele.nicType === type) {
return {
...ele,
rate: Number(value)
};
} else {
return ele;
}
});
this.setState({ items: processedItems });
};
It can be refactored of course, I left it like this to better illustrate the idea. Which is, instead of cloning the items before mapping, or modifying its content, you can return a new object from the map's callback and assign the result to a new variable.

react setState array of object using map doesn't work?

I have problem doing a setState changing value of a nested array of object. Below code suppose
to change question of id 2 to answer: true but it did not, what's wrong?
this.state = {
questions: [
{
id: 1,
answer: ''
},
{
id: 2,
answer: ''
},
]
}
//I have a click event somewhere
this.setState(
{
questions: this.state.questions.map(q => {
if (q.id === 2) {
return {
...q,
answer: true
}
} else {
return { ...q }
}
})
},
console.log(this.state.questions[1]) // did not see id of 2 being changed to true?
)
The console.log(this.state.questions[1]) line is executed before the this.setState line is executed, that's why the old state is printed to the console. You should put the line inside a function to delay the execution:
this.setState(..., () => console.log(this.state.questions[1]));
Also it is recommended to use a function as the first argument if the changed state is derived from the current state because React doesn't apply the new state immediately therefore this.state can be outdated when React applies the new state:
this.setState(state => ({
questions: state.questions.map(q => {
if (q.id === 2) {
return {...q, answer: true};
}
return q;
})
}), () => {
console.log(this.state.questions[1]);
});
You are not invoking your setState callback. Try like this:
this.setState(
{
questions: this.state.questions.map(q => {
if (q.id === 2) {
return {
...q,
answer: true
};
}
return { ...q };
})
},
() => console.log(this.state.questions[1]) // did not see id of 2 being changed to true?
);
Though, since you are using the current state to update your state again, it would be better to use functional setState.
this.setState(
currentState => ({
questions: currentState.questions.map(q => {
if (q.id === 2) {
return {
...q,
answer: true
};
}
return { ...q };
})
}),
() => console.log(this.state.questions[1])
);
Also, you don't have to log your state in a callback to setState. You can log your state in your render method without struggling setState's callback.
this.setState(
currentState => ({
questions: currentState.questions.map(q => {
if (q.id === 2) {
return {
...q,
answer: true
};
}
return { ...q };
})
})
);
....
render() {
console.log( this.state );
....
}
I think it's because Array.map returns an array. Try:
this.setState(
{
questions: this.state.questions.map(q => {
if (q.id === 2) {
q.answer = true;
}
return q;
})
},
console.log(this.state.questions[1]) // did not see id of 2 being changed to true?
)

React not passing state to child via props

I am trying to pass my parent App state to a child component Chart.
App
constructor() {
super();
this.state = {
dataPoints: {
'424353': {
date: '10/10/2016',
qty: '95'
},
'535332': {
date: '10/11/2016',
qty: '98'
},
'3453432': {
date: '10/01/2017',
qty: '94'
}
}
};
this.addPoint = this.addPoint.bind(this);
}
addPoint(dataPoint) {
let dataPoints = {...this.state.dataPoints};
const timestamp = Date.now();
dataPoints[timestamp] = dataPoint;
this.setState({ dataPoints: dataPoints });
console.log('new state', this.state.dataPoints);
}
render() {
return (
<div className="app">
<Chart dataPoints={this.state.dataPoints} />
<FormControl addPoint={this.addPoint} />
</div>
);
}
Chart
composeData() {
Object
.keys(this.props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
componentWillUpdate() {
this.composeData();
}
The addPoint method works, i.e. I can see in the React console that the new datapoint is added to the state. But it is not reflected in the Chart component. More oddly (to me) is the fact that when I've added a point, the console.log line in my addPoint method (above):
console.log('new state', this.state.dataPoints)
does not show the new data point.
In add point
addPoint(dataPoint) {
let dataPoints = {...this.state.dataPoints};
const timestamp = Date.now();
dataPoints[timestamp] = dataPoint;
this.setState({ dataPoints: dataPoints });
console.log('new state', this.state.dataPoints);
}
In the above code you do not see the updated value because setState takes time to mutate, You must log it in the setState call back
this.setState({ dataPoints: dataPoints }, function(){
console.log('new state', this.state.dataPoints);
});
And also in the Chart component you need to bind the composeData function if you are using this.props inside it like
composeData = () =>{
Object
.keys(this.props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
However componentWillMount is only called once and hence you will want to call the composeData function from componentWillReceiveProps also like
componentWillReceiveProps(nextProps) {
this.composeData(nextProps)
}
componentWillMount() {
this.composeData(this.props.dataPoints)
}
composeData(props){
Object
.keys(props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
Because setState is asynchronous, use this you see the updated values:
this.setState({ dataPoints: dataPoints }, () => {
console.log('new state', this.state.dataPoints);
});
Second thing is, whenever any change happen to props values, componentWillReceiveProps lifecycle method will get called, do the computation in this method once you add the new item. Like this:
componentWillReceiveProps(newProps) {
console.log('update props values', newProps);
Object
.keys(newProps.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
Check this answer for complete explanation: why setState is asynchronous.

Categories

Resources