React state is updating without calling setState - reactjs

My state is changed even i have created new variable and without calling setState.
This is my code
changeName = (event, id) => {
const persons = [...this.state.persons]; // Create a copy of array using spread operator
const person = persons.find(cur => cur.id === id);
person.name = event.target.value;
console.log(persons);
console.log(this.state.persons);
}
render() {
let allPersonsArr = this.state.persons.map(cur => {
return <Person name={cur.name} age={cur.age} job={cur.job} key={cur.id} change={(event) => this.changeName(event, cur.id)}/>;
});
return(
<div>
{allPersonsArr}
</div>
);
}
the state.persons has changed upon checking into the console after using person.name = event.target.value even though i'm pointing to the new array persons

Spread syntax just creates a one level deep copy of the array and not a deep copy and since you have objects inside your arrays, setting person.name changes the original object
changeName = (event, id) => {
const persons = this.state.persons(person => {
if (person.id === id) {
return {...person, name: event.target.value}
}
return persons;
})
this.setState({ persons })
}

Your code actually not change the main object of your state. It just edits a (not deep) copy of the state.
To have an original update you should call setState. Also to be sure that your updates do not effect on a copy, you can use React Immutability Helpers.

Related

React setState hook not updating dependent element if passed a variable as opposed to explicit text

I'm right on the verge of tossing React and just using vanilla JS but thought I'd check here first. I'm simply trying to pass the contents of a variable, which contains an object, into state and have that update the element that depends upon it. If I pass setState a variable containing the object, it doesn't work. If I pass it the explicit text of the object it does.
Using React v 18.0.0
function buttonHandler(e) {
e.preventDefault()
let tmpObject = {...chartData}
tmpObject.datasets[0].data = quoteData.map(entry => entry['3'])
tmpObject.datasets[1].data = quoteData.map(({fastEma}) => fastEma)
tmpObject.datasets[2].data = quoteData.map(({slowEma}) => slowEma)
tmpObject.labels = quoteData.map(entry => new Date(entry.timestamp).toLocaleTimeString())
console.log("from button:", tmpObject)
setChartData(prevState => {
console.log("tmpObject",tmpObject)
return tmpObject
})
return <div>
<button onClick={buttonHandler}>Update</button>
<Line options={chartOptions} data={chartData}/>
</div>
When I run the above, the output of the console.log is exactly as it should be but the element does not update. If I copy the object output from the console and paste it explicitly into the code it does work.
function buttonHandler(e) {
e.preventDefault()
setChartData({...})
I've tried every imaginable variation on the below statement to no avail...
return {...prevState, ...tmpObject}
I'd greatly appreciate any suggestions.
EDIT:
As another test, I added the following HTML element to see if it got updated. It gets updated and shows the expected data. Still, I'm having a hard time understanding why the chart will update if I pass it explicit text but will not if I pass it a variable.
<p>{`${new Date().toLocaleTimeString()} {JSON.stringify(chartData)}`</p>
The issue is that of state mutation. Even though you've shallow copied the chartData state you should keep in mind that this is a copy by reference. Each property is still a reference back into the original chartData object.
function buttonHandler(e) {
e.preventDefault();
let tmpObject = { ...chartData }; // <-- shallow copy ok
tmpObject.datasets[0].data = quoteData.map(entry => entry['3']); // <-- mutation!!
tmpObject.datasets[1].data = quoteData.map(({ fastEma }) => fastEma); // <-- mutation!!
tmpObject.datasets[2].data = quoteData.map(({ slowEma }) => slowEma); // <-- mutation!!
tmpObject.labels = quoteData.map(
entry => new Date(entry.timestamp).toLocaleTimeString()
);
console.log("from button:", tmpObject);
setChartData(prevState => {
console.log("tmpObject",tmpObject);
return tmpObject;
});
}
In React not only does the next state need to be a new object reference, but so does any nested state that is being update.
See Immutable Update Pattern - It's a Redux doc but really explains why using mutable updates is key in React.
function buttonHandler(e) {
e.preventDefault();
setChartData(chartData => {
const newChartData = {
...chartData, // <-- shallow copy previous state
labels: quoteData.map(
entry => new Date(entry.timestamp).toLocaleTimeString()
),
datasets: chartData.datasets.slice(), // <-- new datasets array
};
newChartData.datasets[0] = {
...newChartData.datasets[0], // <-- shallow copy
data: quoteData.map(entry => entry['3']), // <-- then update
};
newChartData.datasets[1] = {
...newChartData.datasets[1], // <-- shallow copy
data: quoteData.map(({ fastEma }) => fastEma), // <-- then update
};
newChartData.datasets[2] = {
newChartData.datasets[2], // <-- shallow copy
data: quoteData.map(({ slowEma }) => slowEma), // <-- then update
};
return newChartData;
});
}
Check your work with an useEffect hook with a dependency on the chartData state:
useEffect(() => {
console.log({ chartData });
}, [chartData]);
If there's still updating issue then check the code of the Line component to see if it's doing any sort of mounting memoization of the passed data prop.

ReactJS, update State with handler but data is empty

I have this situation:
I want to update some state (an array) which is used to map different React components.
Those componentes, have their own handleUpdate.
But when I call to handleUpdate the state that I need to use is empty. I think is because each handler method was mounted before the state was filled with data, but then, how could I ensure or use the data in the handler? In other words, the handler needs to update the state that fill it's own state:
const [data, setData] = useState([]);
const [deliver, setDeliver] = useState({items: []});
const handleUpdate = (value, position) => {
// This set works
setDeliver({
items: newItems
});
// This doesn't work because "data" is an empty array - CRASH
setData(data[position] = value);
};
useEffect(() => {
const dataWithComponent = originalData.map((item, i) => ({
...item,
entregado: <SelectorComponent
value={deliver?.items[i].delivered}
key={i}
onUpdate={(value) => handleUpdate(value, i)}
/>
}));
setData(dataWithComponent); // This is set after <SelectComponent is created...
}
}, [originalData]);
The value that you pass don't come from originalData, so the onUpdated don't know what it's value
You run on originalData using map, so you need to pass item.somthing the the onUpdate function
const dataWithComponent = originalData.map((item, i) => ({
...item,
entregado: <SelectorComponent
value={deliver?.items[i].delivered} // you can't use items use deliver.length > 0 ? [i].delivered : ""
key={i}
onUpdate={() => handleUpdate("here you pass item", i)}
/>
}));
I'm not sure, but I think you can do something like that. I hope you get some idea to work it.
// This doesn't work because "data" is an empty array - CRASH
let tempData = [...data].
tempData[position] = value;
setData(tempData);

My component is mutating its props when it shouldn't be

I have a component that grabs an array out of a prop from the parent and then sets it to a state. I then modify this array with the intent on sending a modified version of the prop back up to the parent.
I'm confused because as I modify the state in the app, I console log out the prop object and it's being modified simultaneously despite never being touched by the function.
Here's a simplified version of the code:
import React, { useEffect, useState } from 'react';
const ExampleComponent = ({ propObj }) => {
const [stateArr, setStateArr] = useState([{}]);
useEffect(() => {
setStateArr(propObj.arr);
}, [propObj]);
const handleStateArrChange = (e) => {
const updatedStateArr = [...stateArr];
updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value);
setStateArr(updatedStateArr);
}
console.log(stateArr, propObj.arr);
return (
<ul>
{stateArr.map((stateArrItem, index) => {
return (
<li key={`${stateArrItem._id}~${index}`}>
<label htmlFor={`${stateArrItem.name}~name`}>{stateArrItem.name}</label>
<input
name={`${stateArrItem.name}~name`}
id={`${stateArrItem._id}~input`}
type="number"
value={stateArrItem.keyValue}
data-index={index}
onChange={handleStateArrChange} />
</li>
)
})}
</ul>
);
};
export default ExampleComponent;
As far as I understand, propObj should never change based on this code. Somehow though, it's mirroring the component's stateArr updates. Feel like I've gone crazy.
propObj|stateArr in state is updated correctly and returns new array references, but you have neglected to also copy the elements you are updating. updatedStateArr[e.target.dataset.index].keyValue = parseInt(e.target.value); is a state mutation. Remember, each element is also a reference back to the original elements.
Use a functional state update and map the current state to the next state. When the index matches, also copy the element into a new object and update the property desired.
const handleStateArrChange = (e) => {
const { dataset: { index }, value } = e.target;
setStateArr(stateArr => stateArr.map((el, i) => index === i ? {
...el,
keyValue: value,
} : el));
}

What is proper way to call function from onClick so it don't trigger wrong one?

When I click on button then onClick triggers correct function, run half through and jumps to other function which is not related to it and run through half of it and jumps back to first function, runs half trough again and drops error
Uncaught TypeError: _this.state.searchValue.toLowerCase is not a function
Interesting part is that I click other button before which triggers this function with toLowerCase() and there is no errors.
I dont have any idea whats going on here but so far i was trying to remove few lines to see which line cause it because I dont think that line with toLowerCase() realy is the reason. Everything works when I remove lines where is first this.setState.
Here is my function:
( Alerts is used to track where function is at, that how i know
that it run half through only. It never reach alert("DDD").
This function is which is triggered with button onClick like it should be )
onSelect = (e) => {
const data = e.target.getAttribute('data-id');
const itemId = e.target.getAttribute('data-id');
const itemIdState = !this.state[e.target.getAttribute('data-id')];
alert("AAA")
this.setState(state => { // <--- Somehow problem comes from this setState function
const newState = {};
for (const dataId in state) {
newState[dataId] = dataId === data
}
alert("BBB")
return newState
});
alert("CCC")
this.setState(State => ({
[itemId]: itemIdState,
}), function() {
alert("DDD")
if(this.state[itemId] === true){
this.setState({isAnySelected: true})
}else if(this.state[itemId] === false){
this.setState({isAnySelected: false})
}
})
}
This is other function which is triggered by mistake and is not related to other. It is just returning component which is displayed and when I press on its button then i have this issue.
filterSearch = (id, title, path) => {
let name = title.toLowerCase()
let filter = this.state.searchValue.toLowerCase()
if(name.includes(filter)){
return <SearchResult key={id} data-id={id} pName={path} onClick={this.onSelect} selected={this.state[id]} />
}
}
And here is from where filterSearch is triggered. Behind this.props.searchResult is Redux.
{this.props.searchResult ? this.props.searchResult.map(category =>
this.filterSearch(category.id, category.title, category.path)
) : null
}
I think I see what the problem is: in your problematic this.setState, you cast everything in your state to a boolean:
this.setState(state => {
const newState = {};
for (const dataId in state) {
newState[dataId] = dataId === data
}
alert("BBB")
return newState
});
Your for() statement ends up comparing searchValue to data (some kind of ID), which I imagine more often than not will not be the case, so searchValue ends up getting set to false.
And what happens when you try to do .toLowerCase() on a Boolean?
To fix this, consider structuring your state like this:
this.state = {
searchValue: '',
ids: {},
};
Then, replace your problematic this.setState with something like this:
this.setState((state) => {
const newIDs = {
// Create a clone of your current IDs
...state.ids,
};
Object.keys(newIDs).forEach(key => {
newIDs[key] = key === data
});
alert("BBB")
return {
// searchValue will remain untouched
...state,
// Only update your IDs
ids: newIDs,
}
});
What exactly are you wanting to do here?
this.setState(state => {
const newState = {}; // You are initializing an object
for (const dataId in state) {
newState[dataId] = dataId === data // You are putting in an array every property of state that is equal to data
}
return newState
});
So irrevocably, your this.state.searchValue property will be changed to something else, which is of boolean type. So toLowerCase being a function for string.prototype, you will get an error.
You should describe what you where aiming to get here.

Correct way to remove key from react state

I am using the state of a react (v0.14) view to hold key value pairs of unsaved user ids and user objects. For example:
onChange = (user, field) => {
return (event) => {
let newUser = _.clone(this.state[user.uuid] || user);
_.assign(newUser, {[field]: event.target.value});
this.setState({
[user.uuid]: newUser
});
}
}
render() {
let usersJsx = users.map((user, i) => {
return <div key={i}>
<input type="text" value={user.name}
onChange={this.onChange(user, 'name')}/>
</div>;
});
let numberUnsavedUsers = _.keys(this.state).length;
// ... etc
}
This all works perfectly until I come to the save method:
persistUsers = (event) => {
let unsavedUsers = _.toArray(this.state);
updateUsers(unsavedUsers, {
onSuccessCb: (savedUsers) => {
// Would prefer to remove these two lines and replace
// with `this.setState({});` but this doesn't work... i.e.
// the state is left untouched rather than being
// replaced with `{}`. This makes sense. I guess I was hoping
// someone might point me towards a this.replaceState()
// alternative.
this.setState({nothing: true}); // triggers a state change event.
this.state = {}; // wipes out the state.
}
});
}
I've searched around but only found people modifying nested objects or arrays and not top level key values.
You need to use replaceState instead of setState
Update: replaceState is being deprecated. You should follow the recommendation and use setState with null values.
Recommendation:
You should name the data and use setState so you can more easily work with it.
instead of:
//bad
this.setState({
[user.uuid]: newUser
});
use:
//good
this.setState({
newUser: {uuid: user.uuid}
})
If your state was {unsavedUsers: {userData}} instead of {userData} then you could easily setState({unsavedUsers: {}}) without introducing replaceState.
replaceState is an anti-pattern because it is uncommonly used.
Original Answer:
Like setState() but deletes any pre-existing state keys that are not in the newState object.
Documentation
this.replaceState({}) will remove all the objects.

Resources