0

I am relatively new to React/development in general (about 5 months), and I stumbled upon the following problem that has been driving me absolutely crazy. I am wondering if I am being stupid and missing a fundamental concept, or if this is something deeper.

To give you some background on my project and how this problem came up:

I am building a calculator app, very much like the one you get when you search 'calculator' on Google. I am trying to write a function that, every time the user clicks a button on the calculator, saves everything related to the display as an object and adds that object to an array, held in state, of the entire "input history" of the calculator. That way, when the user wants to delete the display values one by one, the calculator can just go back one index in the input history array and go back in time to the previous setting. This is so the calculator can still show the nice dynamically rendered stylings while deleting (exponents, un-matched parentheses, etc.) without having to write a really long, complicated, and almost certainly buggy delete function.

To accomplish this, I am telling my program to, after each change of state (where I keep the display values), add the current settings and display to the input history array. I chose to put this function in the componentDidUpdate method, and ensured it only fires once by only calling it if this.state.inputHistUpdated is false, which happens each time something is added to the display.

Here is my function, inside of the componentDidUpdate() method:

if (!this.state.inputHistUpdated) {
        let newInputHist = {
            currentMath : this.state.currentMath, 
            exponentCount : this.state.exponentCount, 
            spanStr : this.state.spanStr, 
            rightParensArr : this.state.rightParensArr, 
        }
        console.log(newInputHist);

        this.setState((prevState) => {

            return {
                inputHistory : [...prevState.inputHistory, newInputHist],
                inputHistUpdated : true
            }
        })

    }

Each of these value in the input object is potentially impacted by what the user inputs, and appears in some form or another in the display.

The problem is this

The user inputs a 5 into the calculator. newInputHist logs a single object, whose currentMath value is 5. (This is as expected). React DevTools also shows this.state.inputHist to be an array of one object, whose currentMath value is also 5 (as expected). Here is where the problem is: Let's say the user inputs "+" into the calculator. newInputHist logs a single object with a currentMath value of '5 +', which is correct, and a new object has been added to this.state.inputHist whose currentMath value is also '5 +' BUT the currenMath value of the other element in that array, the one we added previously, has also been changed, from '5' to '5 +'. This happens EVERY TIME I update the display; the currentMath property of every single object in the this.state.inputHist array gets updated to the CURRENT currentMath property, instead of saving the one it originally had.

This makes absolutely no sense to me. I've tried other ways of concatenating the object to the array, and have searched up and down Google and SO looking for a solution, but have had trouble finding anything helpful.

Please let me know what you think might be going on here, and if you need more info/code. I wanted to give you enough background, etc. so you can answer my question without adding anything extraneous, but if I didn't feel free to say so.

Here is more of the code:

This function adds the numbers/operations/everything to the display

addToDisplay(input) {
    this.setState((prevState) => {
        let calcArr = prevState.newCalc ? [] : prevState.currentMath
        if (prevState.lastInput=== '□') {
            calcArr.pop(); 
        }

        if (prevState.lastInput === ' - ' && /[×\-÷\+]/.test(prevState.currentMath[prevState.currentMath.length-2])) {
            calcArr[calcArr.length-1] = ' -'

        }

        calcArr.push(input)
        return { 
            currentMath : calcArr,
            calcActive : true,
            lastInput : input,
            newCalc : false, 
            inputHistUpdated : false
        }
    });
}

These two functions deal with the dynamically rendered UI in the display -- adding specially styled right parentheses when the user inputs a left parenthesis, and adding necessary invisible </spans> when the user up one level in an exponent, which has a different styling

  rightParensFunc() {

    let styledParens = `<span class='parens exp-${this.state.exponentCount}'>)</span>`; 

    this.setState((prevState) => {
        return {
            rightParensArr : [styledParens, ...prevState.rightParensArr], 
            inputHistUpdated : false 
        }
    });


}

spanFunc() {
    let counter = this.state.exponentCount; 
    let spans = ''; 
    while (counter > 0) {
        spans = spans + '</span>';
        counter--;
    }; 
    this.setState({
        spanStr : spans,
        inputHistUpdated : false
    })   
}

And then here is my constructor function

class CalcBody extends React.Component {
constructor(props){
    super(props);
    this.state = {
        calcActive : false, 
        angleUnits : 'radians',
        historyVisible : false, 
        inverse : false,
        prevTotal : '0', 
        lastInput : '0',
        currentMath : [],
        exponentCount : 0,
        prevMath : '', 
        rightParensArr : [],
        spanStr : '',  
        calcHistory : [],
        inputHistory : [], 
        inputHistUpdated : true, 
        newCalc : true
    };
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
nickhealy
  • 43
  • 7
  • Can you show more of the code? It seems like all of your objects in state are referencing the same object in memory, but I don't immediately see the culprit in the code given. The output of `console.log(newinputHist)` might be enough. – Brian Thompson Mar 11 '20 at 18:34
  • Sorry, total newb here -- how do I show you that output? You want to see what's in my dev tools? – nickhealy Mar 11 '20 at 18:50
  • No need anymore, I was mostly interested in seeing what the datatypes of `currentMath` and `rightParensArr` were, but you gave me that with the new code. – Brian Thompson Mar 11 '20 at 18:55

2 Answers2

0

It appears that your issue is here:

let newInputHist = {
  currentMath : this.state.currentMath, // Here
  exponentCount : this.state.exponentCount, 
  spanStr : this.state.spanStr, 
  rightParensArr : this.state.rightParensArr, // Here
}

JavaScript arrays and objects are stored in variables as references. You can read a more detailed explanation of the implications in this answer (under the "Explanation" heading).

To solve this issue, you need to create a true copy of the arrays like this:

let newInputHist = {
  currentMath : [...this.state.currentMath],
  exponentCount : this.state.exponentCount, 
  spanStr : this.state.spanStr, 
  rightParensArr : [...this.state.rightParensArr],
}

What happened

Your array in history, and the one used to store the current math were referencing the same array in memory. Meaning a change to one changes both.

How did this fix it

By using the spead syntax [...array], we create a brand new array. No more accidental changes.

Also: In the addToDisplay function, you are mutating state accidentally for the same reason as above. Change to this:

let calcArr = prevState.newCalc ? [] : [...prevState.currentMath]
Community
  • 1
  • 1
Brian Thompson
  • 13,263
  • 4
  • 23
  • 43
  • 1
    You fixed it! Thank you so much. Also, thank you for the very detailed explanation. It'll be very helpful to avoid this problem in the future! Thanks again. – nickhealy Mar 11 '20 at 19:01
0

currentMath is an array (object) and hence being passed by the same reference.

So this is what happens in your app:

Memory Location   |   Variable pointing at it

     xyz                   currentMath

When 5 is added:

let newInputHist = {
  currentMath: this.state.currentMath, 
  // others
}

// value of currentMath is 5
inputHistory = [
  { currentMath: <value points to xyz memory>, // others }
]

When you enter + next, currentMath becomes 5+

inputHistory = [
  { currentMath: <value points to xyz in memory>, // others },
  { currentMath: <value points to xyz in memory>, // others }
]

So basically all the subsequent objects have their currentMath pointing to the same memory location. Once you add a new data, this gets updated and the new value is reflected across all currentMath.

To avoid this, you have to ensure that the memory locations is different.

Approach 1
You can either take a shallow copy like this:

let newInputHist = {
  currentMath : [ ...this.state.currentMath], 
  // keep others as usual
}

Approach 2
Or take a deep copy like this:

let newInputHist = {
  currentMath : JSON.parse(JSON.stringify(this.state.currentMath)), 
  // keep others as usual
}

These should solve your issue. I'd encourage you to read this up.

I'd also like to mention here the scenario where you'd want to go with approach 2. Let's say there is another object inside currentMath:

currentMath = [ { key: value }, <some primtive like string> ]

In this scenario, currentMath[0] and [...currentMath[0]] will point to the same memory location, although currentMath[1] and [...currentMath[1]] will point to different memory locations. You can solve it using deep copy approach.

Shravan Dhar
  • 1,455
  • 10
  • 18