what is the preferred way to mutate a React state? - javascript

Let's say I have a list of plain objects in my this.state.list that I can then use to render a list of children. What then is the right way to insert object into this.state.list?
Below is the only way I think it will work because you can not mutate the this.state directly as mentioned in the doc.
this._list.push(newObject):
this.setState({list: this._list});
This seems ugly to me. Is there a better way?

concat returns a new array, so you can do
this.setState({list: this.state.list.concat([newObject])});
another alternative is React's immutability helper
var newState = React.addons.update(this.state, {
list : {
$push : [newObject]
}
});
this.setState(newState);

setState() can be called with a function as a parameter:
this.setState((state) => ({ list: state.list.concat(newObj) }))
or in ES5:
this.setState(function(state) {
return {
list: state.list.concat(newObj)
}
})

Update 2016
With ES6 you can use:
this.setState({ list: [...this.state.list, ...newObject] });

From the react docs (https://facebook.github.io/react/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous):
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
So you should do this instead:
this.setState((prevState) => ({
contacts: prevState.contacts.concat([contact])
}));

Related

How to get spread operator updating state with React

I've defined my initial state in a Component as follows:
constructor(props) {
super(props);
//this.state = {count: props.initialCount};
this.state = {
timeArray: [],
metawords: '',
description: '',
currentTime: '',
inputFieldsDisabled: true
}
}
I have an event that gets called and I want to just update the metawords property. I was thinking that the following code should work but it does not.
updateInputValue1(evt) {
const newMetawords = "abcd";
this.setState(
[...this.state,{
metawords: evt.target.value
}]
);
Thoughts?
state is an object so updating it the way you are at the moment won't work.
You can simply update only the property you need as:
this.setState({
metawords: evt.target.value,
})
Since you've mentioned spread operator, you can also (but not always necessary) update your state as:
this.setState({
...this.state,
metawords: evt.target.value,
})
or
this.setState(prevState => ({
...prevState,
metawords: evt.target.value,
}))
Should you need more info about it, I recommend you to have a look at ReactJS documentation.
You can use spread operator like this to setState.
this.setState(
{...this.state,
metawords: evt.target.value
})
However since you only want to change a single property, this will also work in your case:
this.setState({metawords: evt.target.value})
Why not simply:
this.setState({ metawords: evt.target.value })
You don't need to pass all other state values, just pass the property and the new value, that you want to update. You don't need to bother about the other state values during setState, React will do the merging (merging of all other state values and the state that you wants to update).
For more details check DOC.

setState is not updating state properly

Sorry, I really miss something with the transmission of state within props of sub components in React.
I have implemented a version of a todo list with 3 components.
There is a Form component and a ListTodo component. The state is stored only in the App component.
import React, {Component} from 'react';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
tasks: ["un truc", "autre truc"]
};
this.addTask = this.addTask.bind(this);
}
addTask(task) {
this.setState({
tasks: this.state.tasks.push(task)
})
}
render() {
return (
<div className="App">
<Form onTaskAdded={ this.addTask }></Form>
<ListTodo tasks={ this.state.tasks }></ListTodo>
</div>
);
}
}
class Form extends Component {
constructor(props) {
super(props);
this.state = {
task: ""
}
this.handleChange = this.handleChange.bind(this);
this.addTask = this.addTask.bind(this);
}
handleChange(event) {
this.setState({
task: event.target.value
});
}
addTask(event) {
this.props.onTaskAdded(this.state.task);
event.preventDefault();
}
render() {
return (
<form onSubmit={ this.addTask }>
<input placeholder="À faire" onChange={ this.handleChange }></input>
<input type="submit"></input>
</form>
)
}
}
class ListTodo extends Component {
render() {
const tasks = this.props.tasks.map((t, i) => (
<li key={i}>{t}</li>
))
return (
<ul>{tasks}</ul>
)
}
}
export default App;
The display is good at start so the ListTodo achieves to see the prop tasks. But after a form submission, I get an error on ListTodo.render :
TypeError: this.props.tasks.map is not a function
When I console.log the this.props.tasks, I don't get my array but the length of the array.
Do you know why?
Edit :
Thanks for answers guys, you're right. I missed the behavior of Array.push.
But React seems still odd. If I let the mistaken code
this.setState({
tasks: this.state.tasks.push(task)
})
then a console.log(JSON.stringify(this.state)) displays :
{"tasks":["un truc","autre truc","aze"]}.
Very disturbing to not be able to trust a console.log...
As per MDN DOC:
The push() method adds one or more elements to the end of an array and
returns the new length of the array.
Array.push never returns the result array, it returns the number, so after adding the first task, this.state.tasks becomes a number, and it is throwing the error when you trying to run map on number.
You did the mistake here:
addTask(task) {
this.setState({
tasks: this.state.tasks.push(task)
})
}
Write it like this:
addTask(task) {
this.setState( prevState => ({
tasks: [...prevState.tasks, task]
}))
}
Another import thing here is, the new state will be depend on the previous state value, so instead of using this.state inside setState, use updater function.
Explanation about Edit part:
Two important things are happening there:
1- setState is async so just after setState we can expect the updated state value.
Check this for more details about async behaviour of setState.
2- Array.push always mutate the original array by pushing the item into that, so you are directly mutating the state value by this.state.tasks.push().
Check the DOC for more details about setState.
Check the MDN Doc for spread operator (...).
Check this snippet:
let a = [1,2,3,4];
let b = a.push(5); //it will be a number
console.log('a = ', a);
console.log('b = ', b);
The problem is in how you add a task in the App’s state.
addTask(task) {
this.setState({
tasks: this.state.tasks.push(task)
})
}
See, Array.prototype.push returns the length of the array after adding an element. What you really want is probably Array.prototype.concat.
addTask(task) {
this.setState({
tasks: this.state.tasks.concat([ task ])
})
}
Also, thanks to #t-j-crowder pointers and as also reported by #mayank-shukla, you should use a different approach to mutate your state:
addTask(task) {
this.setState(function (state) {
return {
tasks: state.tasks.concat([ task ])
}
});
}
Or using ES2015 Arrows, Object destructuring and Array spread:
addTask(task) {
this.setState(({ tasks }) => ({
tasks: [ ...tasks, task ]
}));
}
Since Component.prototype.setState can be asynchronous, passing a function to it will guarantee the new state values depend on the right, current previous values.
This means that if two or more setState calls happen one after another you are this way sure that the result of the first one will be kept by applying the second.
As the other answers stated, the Array push method does not return the array. Just to complement the answers above, if you are using ES6, a nice and elegant way of doing this is using the spread operator (you can read more about it here)
this.setState({
tasks: [...this.state.tasks, task]
})
It is essentially the same as using the concat method, but I think this has a nicer readability.
The problem is from Array.push return the number of elements in the array and not the updated array
addTask(task) {
this.setState({
tasks: this.state.tasks.push(task)
})
}
To fix this you can push to state.tasks then setState with it later on:
addTask(task) {
this.state.tasks.push(task);
this.setState({
tasks: this.state.tasks
})
}
This way you set state.task to the updated array.

Updating React state with Immutable

I'm trying to update my regular React state through Immutable, and got into some few issues. The state isn't deeply nested or it isn't nested from anything other than the state itself, such as { "username" : "keyval" : null}}
This means I could not do something such as username.update('keyval', something), instead I need another approach. Its a rather easy question, I just don't know how to do it. Here's how my setState looks like, which I want to make an Immutable setState action.
handleUpdatePassword(event) {
event.persist()
this.setState(({password}) => ({
password: state.update('password', event.target.value)
})
);
}
And here is the error I get when trying:
handleUpdatePassword(event) {
event.persist()
this.setState({
password: state.update('password', event.target.value)
})
}
Also, This works, but I get this error: this.state.updater is not a function
handleUpdateUsername(event) {
console.log(this.state)
event.persist()
this.setState({
username: this.state.update('username', event.target.value)
})
}
state should be a plain JavaScript object as you can read in the documentation.
Note that state must be a plain JS object, and not an Immutable
collection, because React's setState API expects an object literal and
will merge it (Object.assign) with the previous state.
Your initial state should look something like this
constructor(){
...
this.state = {data: Map({ password: "", username: ""})}
}
After that, you'll be able to update the data like this
handleUpdatePassword(event) {
this.setState(({data}) => ({
data: data.update('password', password => event.target.value)
}));
}
You are creating explicit objects. Just let ImmutableJS do it for you.
class YourReactComp extends React.Component {
constructor() {
this.state = Immutable.Map({"username": ""});
}
handleUpdateUsername(event) {
console.log(this.state)
event.persist()
this.setState(this.state.set("username", event.target.value));
}
}
EDIT
ImmutableMap.update(key, updater) uses a callback to set the value, you want ImmutableMap.set(key, newValue) here.

How do I clear states or empty array on click in react 0.14 ES6?

I am rendering an array in a modal. Once the modal closes I need to empty the array.The following code updates the array but not clear array on click of closeModal.
constructor(props,context) {
super(props,context);
this.state = {
myArray: []
};
}
pushData(newVar) {
this.setState((state) => {
myArray: state.myArray.push(newVar)
});
}
closeModal() {
this.setState({
myArray: []
})
}
I found the problem is my closeModal didn’t get called at all on closing modal. I am doing that to closeModal on componentWillUnmount function.
I understood that the below code causes problem.
this.state.myArray=[] // class component
const[myArray, setMyArray]=useState([]) // functional component
I changed it back to
this.setState({myArray: []}); // class component
setMyArray([]); // functional component
You can as well use this to clear array without using setState:
this.state.your_array.length = 0;
This will work in any function.
Update
Functional component:
setYourArray([])
A couple of solutions with explanations to this (albeit in ES5) can be found here:
https://stackoverflow.com/a/29994490/4572987
this.state.array.splice();
This will Delete or truncate whole array
This is what worked for me.
Class based component
this.setState({myArray: []});
With React Hook,
const [myArray, setMayArray] = React.useState([]);
setMayArray([]);

this.setState isn't merging states as I would expect

I have the following state:
this.setState({ selected: { id: 1, name: 'Foobar' } });
Then I update the state:
this.setState({ selected: { name: 'Barfoo' }});
Since setState is suppose to merge I would expect it to be:
{ selected: { id: 1, name: 'Barfoo' } };
But instead it eats the id and the state is:
{ selected: { name: 'Barfoo' } };
Is this expected behavior and what's the solution to update only one property of a nested state object?
I think setState() doesn't do recursive merge.
You can use the value of the current state this.state.selected to construct a new state and then call setState() on that:
var newSelected = _.extend({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });
I've used function _.extend() function (from underscore.js library) here to prevent modification to the existing selected part of the state by creating a shallow copy of it.
Another solution would be to write setStateRecursively() which does recursive merge on a new state and then calls replaceState() with it:
setStateRecursively: function(stateUpdate, callback) {
var newState = mergeStateRecursively(this.state, stateUpdate);
this.replaceState(newState, callback);
}
Immutability helpers were recently added to React.addons, so with that, you can now do something like:
var newState = React.addons.update(this.state, {
selected: {
name: { $set: 'Barfoo' }
}
});
this.setState(newState);
Immutability helpers documentation.
Since many of the answers use the current state as a basis for merging in new data, I wanted to point out that this can break. State changes are queued, and do not immediately modify a component's state object. Referencing state data before the queue has been processed will therefore give you stale data that does not reflect the pending changes you made in setState. From the docs:
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.
This means using "current" state as a reference in subsequent calls to setState is unreliable. For example:
First call to setState, queuing a change to state object
Second call to setState. Your state uses nested objects, so you want to perform a merge. Before calling setState, you get current state object. This object does not reflect queued changes made in first call to setState, above, because it's still the original state, which should now be considered "stale".
Perform merge. Result is original "stale" state plus new data you just set, changes from initial setState call are not reflected. Your setState call queues this second change.
React processes queue. First setState call is processed, updating state. Second setState call is processed, updating state. The second setState's object has now replaced the first, and since the data you had when making that call was stale, the modified stale data from this second call has clobbered the changes made in the first call, which are lost.
When queue is empty, React determines whether to render etc. At this point you will render the changes made in the second setState call, and it will be as though the first setState call never happened.
If you need to use the current state (e.g. to merge data into a nested object), setState alternatively accepts a function as an argument instead of an object; the function is called after any previous updates to state, and passes the state as an argument -- so this can be used to make atomic changes guaranteed to respect previous changes.
I didn't want to install another library so here's yet another solution.
Instead of:
this.setState({ selected: { name: 'Barfoo' }});
Do this instead:
var newSelected = Object.assign({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });
Or, thanks to #icc97 in the comments, even more succinctly but arguably less readable:
this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });
Also, to be clear, this answer doesn't violate any of the concerns that #bgannonpl mentioned above.
Preserving the previous state based on #bgannonpl answer:
Lodash example:
this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }));
To check that it's worked properly, you can use the second parameter function callback:
this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }), () => alert(this.state.selected));
I used merge because extend discards the other properties otherwise.
React Immutability example:
import update from "react-addons-update";
this.setState((previousState) => update(previousState, {
selected:
{
name: {$set: "Barfood"}
}
});
As of right now,
If the next state depends on the previous state, we recommend using
the updater function form, instead:
according to documentation https://reactjs.org/docs/react-component.html#setstate, using:
this.setState((prevState) => {
return {quantity: prevState.quantity + 1};
});
My solution for this kind of situation is to use, like another answer pointed out, the Immutability helpers.
Since setting the state in depth is a common situation, I've created the folowing mixin:
var SeStateInDepthMixin = {
setStateInDepth: function(updatePath) {
this.setState(React.addons.update(this.state, updatePath););
}
};
This mixin is included in most of my components and I generally do not use setState directly anymore.
With this mixin, all you need to do in order to achieve the desired effect is to call the function setStateinDepth in the following way:
setStateInDepth({ selected: { name: { $set: 'Barfoo' }}})
For more information:
On how mixins work in React, see the official documentation
On the syntax of the parameter passed to setStateinDepth see the Immutability Helpers documentation.
I am using es6 classes, and I ended up with several complex objects on my top state and was trying to make my main component more modular, so i created a simple class wrapper to keep the state on the top component but allow for more local logic.
The wrapper class takes a function as its constructor that sets a property on the main component state.
export default class StateWrapper {
constructor(setState, initialProps = []) {
this.setState = props => {
this.state = {...this.state, ...props}
setState(this.state)
}
this.props = initialProps
}
render() {
return(<div>render() not defined</div>)
}
component = props => {
this.props = {...this.props, ...props}
return this.render()
}
}
Then for each complex property on the top state, i create one StateWrapped class. You can set the default props in the constructor here and they will be set when the class is initialised, you can refer to the local state for values and set the local state, refer to local functions, and have it passed up the chain:
class WrappedFoo extends StateWrapper {
constructor(...props) {
super(...props)
this.state = {foo: "bar"}
}
render = () => <div onClick={this.props.onClick||this.onClick}>{this.state.foo}</div>
onClick = () => this.setState({foo: "baz"})
}
So then my top level component just needs the constructor to set each class to it's top level state property, a simple render, and any functions that communicate cross-component.
class TopComponent extends React.Component {
constructor(...props) {
super(...props)
this.foo = new WrappedFoo(
props => this.setState({
fooProps: props
})
)
this.foo2 = new WrappedFoo(
props => this.setState({
foo2Props: props
})
)
this.state = {
fooProps: this.foo.state,
foo2Props: this.foo.state,
}
}
render() {
return(
<div>
<this.foo.component onClick={this.onClickFoo} />
<this.foo2.component />
</div>
)
}
onClickFoo = () => this.foo2.setState({foo: "foo changed foo2!"})
}
Seems to work quite well for my purposes, bear in mind though you can't change the state of the properties you assign to wrapped components at the top level component as each wrapped component is tracking its own state but updating the state on the top component each time it changes.
Solution
Edit: This solution used to use spread syntax. The goal was make an object without any references to prevState, so that prevState wouldn't be modified. But in my usage, prevState appeared to be modified sometimes. So, for perfect cloning without side effects, we now convert prevState to JSON, and then back again. (Inspiration to use JSON came from MDN.)
Remember:
State updates are shallow: they only go one level deep
State shouldn't be directly mutated. Instead, use setState(prevState => stateChange)
Steps
Make a copy of the root-level property of state that you want to change
Mutate this new object
Create an update object
Return the update
Steps 3 and 4 can be combined on one line.
Example
this.setState(prevState => {
var newSelected = JSON.parse(JSON.stringify(prevState.selected)) //1
newSelected.name = 'Barfoo'; //2
var update = { selected: newSelected }; //3
return update; //4
});
Simplified example:
this.setState(prevState => {
var selected = JSON.parse(JSON.stringify(prevState.selected)) //1
selected.name = 'Barfoo'; //2
return { selected }; //3, 4
});
This follows the React guidelines nicely. Based on eicksl's answer to a similar question.
ES6 solution
We set the state initially
this.setState({ selected: { id: 1, name: 'Foobar' } });
//this.state: { selected: { id: 1, name: 'Foobar' } }
We are changeing a property on some level of the state object:
const { selected: _selected } = this.state
const selected = { ..._selected, name: 'Barfoo' }
this.setState({selected})
//this.state: { selected: { id: 1, name: 'Barfoo' } }
React state doesn't perform the recursive merge in setState while expects that there won't be in-place state member updates at the same time. You either have to copy enclosed objects/arrays yourself (with array.slice or Object.assign) or use the dedicated library.
Like this one. NestedLink directly supports handling of the compound React state.
this.linkAt( 'selected' ).at( 'name' ).set( 'Barfoo' );
Also, the link to the selected or selected.name can be passed everywhere as a single prop and modified there with set.
have you set the initial state?
I'll use some of my own code for example:
getInitialState: function () {
return {
dragPosition: {
top : 0,
left : 0
},
editValue : "",
dragging : false,
editing : false
};
}
In an app I'm working on, this is how I've been setting and using state. I believe on setState you can then just edit whatever states you want individually I've been calling it like so:
onChange: function (event) {
event.preventDefault();
event.stopPropagation();
this.setState({editValue: event.target.value});
},
Keep in mind you have to set the state within the React.createClass function that you called getInitialState
I use the tmp var to change.
changeTheme(v) {
let tmp = this.state.tableData
tmp.theme = v
this.setState({
tableData : tmp
})
}

Categories