I have this code in my constructor:
this.state = {
tests: [
{
question: "1",
answer: "2",
user: ""
},
{
question: "1",
answer: "2",
user: ""
},
],
};
I have edit function where I read event value in my input:
edit(id, event) {
this.state.tests[id].user = event.target.value;
this.setState({tests:this.state.tests});
}
But es give me this warning:
Do not mutate state directly. Use setState()
react/no-direct-mutation-state
What can i do in this case? Maybe somehow change the line with the assignment event.target.value into setState()?
You can use map() to create copy of tests
edit(id, event) {
const user = event.target.value;
const tests = this.state.tests.map((x,i) => i === id ? {...x, user} : x);
this.setState({tests});
}
One way I tend to go is to make a copy of the array first and then change an item in it, or change the array itself, and then set the state
var tests = this.state.tests.slice(0);
tests[id].user = event.target.value;
this.setState({tests:tests});
You may want to deep-clone the array in some cases, sometimes not.
You are correct, that the problem is with the line:
this.state.tests[id].user = event.target.value;
That's the point where you are mutating your state directly.
You have a few options.
You can "clone" the array first and then update it:
const newTests = [...this.state.tests];
newTests[id].user = event.target.value;
this.setState({tests: newTests});
You could also use immutability-helper:
const {value} = event.target;
this.setState(prevState => update(prevState, {[id]: {user: {$set: value}}}));
In this example, you need to extract value from your event, because it's not safe to access event values in asynchronous calls after an event has been handled.
edit(id, event) {
var newNote = {...this.state.tests[id]}
newNote.user = event.target.value
this.setState({ tests: [...this.state.tests, newNote]})
}
First of all, when you try to set a the new state using the data from previous state you have to use the updater as a function
https://reactjs.org/docs/react-component.html#setstate
const edit = (id, event) => {
this.setState((prevState) => {
const tests = [...prevState.tests];
tests[id] = {
...tests[id],
user: event.target.value
};
return {
...prevState,
tests
};
});
};
When state is not heavy, I use the following codes:
edit (id, event) {
const cp = JSON.parse(JSON.stringify(this.state.tests))
cp[id].user = event.target.value
this.setState({ tests: cp })
}
Update: I found Immer solves it perfectly.
import produce from 'immer'
edit (id, event) {
this.setState(
produce(draft => draft.tests[id].user = event.target.value)
)
}
Related
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.
I am trying to test a React component which uses one of the overloads for setState, but am unsure how to assert the call correctly. An example component would be:
class CounterComponent extends React.Component {
updateCounter() {
this.setState((state) => {
return {
counterValue: state.counterValue + 1
};
});
}
}
The assumption here is that this method will be called asyncronously, so cannot rely on the current state, outwith the call to setState (as it may change before setState executes). Can anyone suggest how you would assert this call? The following test fails as it is simply comparing the function names.
it("Should call setState with the expected parameters", () => {
const component = new CounterComponent();
component.setState = jest.fn(() => {});
component.state = { counterValue: 10 };
component.updateCounter();
const anonymous = (state) => {
return {
counterValue: state.counterValue + 1
};
};
//expect(component.setState).toHaveBeenCalledWith({ counterValue: 11 });
expect(component.setState).toHaveBeenCalledWith(anonymous);
});
Edit: Given yohai's response below, i will add some further context as I feel i may have over simplified the problem however i do not want to re-write the entire question for clarity.
In my actual component, the state value being edited is not a simple number, it is an array of objects with the structure:
{ isSaving: false, hasError: false, errorMessage: ''}
and a few other properties. When the user clicks save, an async action is fired for each item in the array, and then the corresponding entry is updated when that action returns or is rejected. As an example, the save method would look like this:
onSave() {
const { myItems } = this.state;
myItems.forEach(item => {
api.DoStuff(item)
.then(response => this.handleSuccess(response, item))
.catch(error => this.handleError(error, item));
});
}
The handle success and error methods just update the object and call replaceItem:
handleSuccess(response, item) {
const updated = Object.assign({}, item, { hasSaved: true });
this.replaceItem(updated);
}
handleError(error, item) {
const updated = Object.assign({}, item, { hasError: true });
this.replaceItem(updated);
}
And replaceItem then replaces the item in the array:
replaceItem(updatedItem) {
this.setState((state) => {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
return {
myItems: working
};
});
}
replaceItem is the method I am trying to test, and am trying to validate that it calls setState with the correct overload and a function which correctly updated the state.
My answer below details how I have solved this for myself,but comments and answers are welcome =)
#Vallerii: Testing the resulting state does seem a simpler way, however if i do, there is no way for the test to know that the method is not doing this:
replaceItem(updatedItem) {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
this.setState({ myItems: working });
}
When replaceItem does not use the correct overload for setState, this code fails when called repeatedly as (I assume) react is batching updates and the state this version uses is stale.
I think you should test something a little bit different and it will look somthing like this (I'm using enzyme):
import React from 'react'
import { mount } from 'enzyme'
import CounterComponent from './CounterComponent'
it("Should increase state by one", () => {
const component = mount(<CounterComponent />)
const counter = 10;
component.setState({ counter });
component.instance().updateCounter();
expect(component.state().counter).toEqual(counter + 1);
});
I have come up with a solution to this after some further thought. I am not sure it is the best solution, but given that the updateCounter method in the example above passes a function into the setState call, I can simply get a reference to that function, execute it with a known state and check the return value is correct.
The resulting test looks like this:
it("Should call setState with the expected parameters", () => {
let updateStateFunction = null;
const component = new CounterComponent();
component.setState = jest.fn((func) => { updateStateFunction = func;});
component.updateCounter();
const originalState = { counterValue: 10 };
const expectedState = { counterValue: 11};
expect(component.setState).toHaveBeenCalled();
expect(updateStateFunction(originalState)).toEqual(expectedState);
});
I have a state object that looks like this.
this.state = {
formValues: {},
};
After some processing, formValues contains the following.
this.state = {
formValues: {
q1: value 1,
q2: value 2
},
};
Now i have q3 inside formValues which is an array of values. When i try to push the value like as follows
let q3 = e.target.name,
arrayValues = [1,2,3]
formValues[q3].push(arrayValues)
I am getting the following error while submitting the data
Uncaught Error: A state mutation was detected between dispatches
It looks like there is a problem with pushing data into array. Any idea on how to fix this?
You need to create a copy and update the state with setState instead of direct state mutation with push.
this.setState(prevState => ({
formValues: {
...prevState.formValues,
[q3]: prevState.formValues[q3].concat(arrayValues),
},
}));
assuming that you always want to push to your array such as in your use case:
const { formValues } = this.state
const arrayValues = [1,2,4]
const newFormValues = { ...formValues, q3: [...formValues[q3], arrayValues]}
this.setState({ formValues: newFormValues })
but much better if you control directly the value of q3:
const { formValues } = this.state
const arrayValues = [1,2,4]
const newFormValues = { ...formValues, q3: arrayValues}
this.setState({ formValues: newFormValues })
state default values
state = {
moveType: {
value: 0,
open: false,
completed: false
}
};
// callback to update new state
let step = 'moveType';
let val = 3; // new value
let newObj = { ...this.state[step], value: val };
console.log(newObj);
this.setState({[step]: newObj }, function () {console.log(this.state);});
console.log(newObj) shows new values proper, but this.state still shows old values.. can you tell me what i'm doing wrong?
Setting state in react is pretty sensitive thing to do.
The best practices I've used to is always control object deep merge manually and use this.setState(state => { ... return new state; }) type of call, like in this example:
this.setState(state => ({
...state,
[step]: { ...(state[step] || {}), ...newObj },
}), () => console.log(this.state));
SNIPPET UPDATE start
[step]: { ...state[step], ...newObj }
Changed to:
[step]: { ...(state[step] || {}), ...newObj }
To deal correctly with cases, when state does not have this step key yet
SNIPPET UPDATE end
Thing is, that when you use this.state (in let newObj = { ...this.state[step]), it might have an outdated value, due to some pending (not merged yet) changes to the state, that you've called just couple of milliseconds ago.
Thus I recommend to use callback approach: this.setState(state => { ... use state and return new state;}) which guarantees that the state you use has latest value
EDIT: this is a duplicate, see here
I can't find any examples of using a dynamic key name when setting the state. This is what I want to do:
inputChangeHandler : function (event) {
this.setState( { event.target.id : event.target.value } );
},
where event.target.id is used as the state key to be updated. Is this not possible in React?
Thanks to #Cory's hint, i used this:
inputChangeHandler : function (event) {
var stateObject = function() {
returnObj = {};
returnObj[this.target.id] = this.target.value;
return returnObj;
}.bind(event)();
this.setState( stateObject );
},
If using ES6 or the Babel transpiler to transform your JSX code, you can accomplish this with computed property names, too:
inputChangeHandler : function (event) {
this.setState({ [event.target.id]: event.target.value });
// alternatively using template strings for strings
// this.setState({ [`key${event.target.id}`]: event.target.value });
}
When you need to handle multiple controlled input elements, you can add a name attribute to each element and let the handler function choose what to do based on the value of event.target.name.
For example:
inputChangeHandler(event) {
this.setState({ [event.target.name]: event.target.value });
}
How I accomplished this...
inputChangeHandler: function(event) {
var key = event.target.id
var val = event.target.value
var obj = {}
obj[key] = val
this.setState(obj)
},
I just wanted to add, that you can also use de-structuring to refactor the code and make it look neater.
inputChangeHandler: function ({ target: { id, value }) {
this.setState({ [id]: value });
},
this.setState({ [`${event.target.id}`]: event.target.value}, () => {
console.log("State updated: ", JSON.stringify(this.state[event.target.id]));
});
Please mind the quote character.
I had a similar problem.
I wanted to set the state of where the 2nd level key was stored in a variable.
e.g. this.setState({permissions[perm.code]: e.target.checked})
However this isn't valid syntax.
I used the following code to achieve this:
this.setState({
permissions: {
...this.state.permissions,
[perm.code]: e.target.checked
}
});
In loop with .map work like this:
{
dataForm.map(({ id, placeholder, type }) => {
return <Input
value={this.state.type}
onChangeText={(text) => this.setState({ [type]: text })}
placeholder={placeholder}
key={id} />
})
}
Note the [] in type parameter.
Hope this helps :)
I was looking for a pretty and simple solution and I found this:
this.setState({ [`image${i}`]: image })
Hope this helps
With ES6+ you can just do [${variable}]
when the given element is a object:
handleNewObj = e => {
const data = e[Object.keys(e)[0]];
this.setState({
anykeyofyourstate: {
...this.state.anykeyofyourstate,
[Object.keys(e)[0]]: data
}
});
};
hope it helps someone
Your state with dictionary update some key without losing other value
state =
{
name:"mjpatel"
parsedFilter:
{
page:2,
perPage:4,
totalPages: 50,
}
}
Solution is below
let { parsedFilter } = this.state
this.setState({
parsedFilter: {
...this.state.parsedFilter,
page: 5
}
});
here update value for key "page" with value 5
try this one please.
State is here as example
this.state = {
name: '',
surname: '',
username: '',
email: '',
}
Onchange function is here.
onChangeData = (type, event) => {
const stateData = this.state;
if (event === "" || event === "Seçiniz")
stateData[type] = undefined
else
stateData[type] = event
this.setState({ stateData});
}
Can use a spread syntax, something like this:
inputChangeHandler : function (event) {
this.setState( {
...this.state,
[event.target.id]: event.target.value
} );
},