Search code examples
reactjsreact-hooksuse-state

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>

Solution

  • 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.