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 is explicitly into the code it does work.
function buttonHandler(e) {
e.preventDefault()
setChartData({...})
I've tried every imaginable variations 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>
CodePudding user response:
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.