0

I am facing the following issue and not able to figure it out.

I have two variables inside the state called userDetails & userDetailsCopy. In componentDidMount I am making an API call and saving the data in both userDetails & userDetailsCopy.

I am maintaining another copy called userDetailsCopy for comparison purposes.

I am updating only userDetails inside setState but even userDetailsCopy is also getting updated instead of have old API data.

Below is the code :

constructor(){
    super()
    this.state={
        userDetails:{},
        userDetailsCopy: {}
    }
}

componentDidMount(){
     // API will return the following data
       apiUserDetails : [
            {
                 'name':'Tom',
                 'age' : '28'
            },
            {
                 'name':'Jerry',
                 'age' : '20'

            }
        ]

       resp.data is nothing but apiUserDetails
    /////

     apiCall()
     .then((reps) => {
         this.setState({ 
             userDetails: resp.data,
             userDetailsCopy: resp.data
         })
     })
}

updateValue = (text,i) => {
     let userDetail = this.state.userDetails
     userDetail[i].name = text
     this.setState({ 
         userDetails: userDetail
     })
}

submit = () => {
     console.log(this.state.userDetials) // returns updated values
     console.log(this.state.userDetailsCopy) // also return updated values instead of returning old API data
}

Need a quick solution on this.
Shariq
  • 83
  • 2
  • 13
  • `userDetails.name = text` this mutates the current state object, which is probably why you see changes on both the `userDetails` and `userDetailsCopy` objects. – Emile Bergeron Jun 02 '20 at 16:13
  • 1
    where have you set userDetailsCopy? – Shubham Khatri Jun 02 '20 at 16:13
  • Does this answer your question? [How to update nested state properties in React](https://stackoverflow.com/questions/43040721/how-to-update-nested-state-properties-in-react) – Emile Bergeron Jun 02 '20 at 16:13
  • And here's [why you shouldn't mutate the current state object.](https://stackoverflow.com/q/37755997/1218980) – Emile Bergeron Jun 02 '20 at 16:14
  • And here's [why `let userDetail = this.state.userDetails` is not enough to make a copy.](https://stackoverflow.com/q/518000/1218980) – Emile Bergeron Jun 02 '20 at 16:17
  • @ShubhamKhatri I am setting userDetialsCopy only in componentDidMount and rest of the code I am not changing userDetailsCopy the only thing which I am changing is userDetails. But when userDetails are changes even userDetailsCopy is getting changed. – Shariq Jun 02 '20 at 16:21
  • You haven't showed how you set it, that is an imporant information – Shubham Khatri Jun 02 '20 at 16:25
  • @ShubhamKhatri apologies, I have modified the code. Please do help me with a solution – Shariq Jun 02 '20 at 16:27

2 Answers2

2

The problem with this is that you think you are making a copy of the object in state by doing this

     let userDetail = this.state.userDetails
     userDetail.name = text

But, in Javascript, objects are not copied like this, they are passed by referrence. So userDetail at that point contains the referrence to the userDetails in your state, and when you mutate the userDetail it goes and mutates the one in the state.

ref: https://we-are.bookmyshow.com/understanding-deep-and-shallow-copy-in-javascript-13438bad941c

To properly clone the object from the state to your local variable, you need to instead do this:

let userDetail = {...this.state.userDetails}

OR

let userDetail = Object.assign({}, this.state.userDetails)

Always remember, Objects are passed by referrence not value.

EDIT: I didn't read the question properly, but the above answer is still valid. The reason userDetailCopy is being updated too is because resp.data is passed by referrence to both of them, and editing any one of them will edit the other.

Danyal
  • 860
  • 7
  • 13
  • I just now tried with your approach and still, userDetailsCopy is getting updated to userDetails. – Shariq Jun 02 '20 at 16:45
  • Show me the places you have changed them in? Did you use deep cloning in the api call too where you assign `resp.data` to `userDetails` and `userDetailsCopy` – Danyal Jun 02 '20 at 16:58
  • Yes @Danyal in ComponentDidMount I have assigned resp.data to userDetails and userDetailsCopy. But, I am not making any changes to userDetailsCopy I am just setting the value from API for comparison purpose – Shariq Jun 02 '20 at 17:00
  • Try setting the resp.data like this and see. ```this.setState({ userDetails: { ...resp.data }, userDetailsCopy: { ...resp.data } })``` Also, you have lots of typos in code, I'm not sure if it's because you have written it here or if it's in your actual code – Danyal Jun 02 '20 at 17:03
  • :( tried above approach but userDetailsCopy is getting updated. – Shariq Jun 02 '20 at 17:12
  • That's extremely odd behaviour, unless it's getting changed some other way, because this code should work. Maybe paste the changes here, let's see. I can't really think of any more bugs in this – Danyal Jun 02 '20 at 17:29
  • I have modified my code in above example. In componentDidMount I have mentioned a sample JSON that resp.data would return – Shariq Jun 02 '20 at 17:47
1

React state and its data should be treated as immutable.

From the React documentation:

Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.

Here are five ways how to treat state as immutable:

Approach #1: Object.assign and Array.concat

updateValue = (text, index) => {
  const { userDetails } = this.state;

  const userDetail = Object.assign({}, userDetails[index]);

  userDetail.name = text;

  const newUserDetails = []
    .concat(userDetails.slice(0, index))
    .concat(userDetail)
    .concat(userDetails.slice(index + 1));

  this.setState({
    userDetails: newUserDetails
  });
}

Approach #2: Object and Array Spread

updateValue = (text, index) => {
  const { userDetails } = this.state;

  const userDetail = { ...userDetails[index], name: text };

  this.setState({
    userDetails: [
      ...userDetails.slice(0, index),
      userDetail,
      ...userDetails.slice(index + 1)
    ]
  });
}

Approach #3: Immutability Helper

import update from 'immutability-helper';

updateValue = (text, index) => {
  const userDetails = update(this.state.userDetails, {
    [index]: {
      $merge: {
        name: text
      }
    }
  });

  this.setState({ userDetails });
};

Approach #4: Immutable.js

import { Map, List } from 'immutable';

updateValue = (text, index) => {
  const userDetails = this.state.userDetails.setIn([index, 'name'], text);

  this.setState({ userDetails });
};

Approach #5: Immer

import produce from "immer";

updateValue = (text, index) => {
  this.setState(
    produce(draft => {
        draft.userDetails[index].name = text;
    })
  );
};

Note: Option #1 and #2 only do a shallow clone. So if your object contains nested objects, those nested objects will be copied by reference instead of by value. So if you change the nested object, you’ll mutate the original object.

To maintain the userDetailsCopy unchanged you need to maintain the immutability of state (and state.userDetails of course).

function getUserDerails() {
  return new Promise(resolve => setTimeout(
    () => resolve([
      { id: 1, name: 'Tom', age : 40 },
      { id: 2, name: 'Jerry', age : 35 }
    ]),
    300
  ));
}

class App extends React.Component {
  state = {
    userDetails: [],
    userDetailsCopy: []
  };

  componentDidMount() {
    getUserDerails().then(users => this.setState({
      userDetails: users,
      userDetailsCopy: users
    }));
  }
  
  createChangeHandler = userDetailId => ({ target: { value } }) => {
    const { userDetails } = this.state;
    
    const index = userDetails.findIndex(({ id }) => id === userDetailId);
    const userDetail = { ...userDetails[index], name: value };

    this.setState({
      userDetails: [
        ...userDetails.slice(0, index),
        userDetail,
        ...userDetails.slice(index + 1)
      ]
    });
  };
  
  render() {
    const { userDetails, userDetailsCopy } = this.state;
    
    return (
      <React.Fragment>
        {userDetails.map(userDetail => (
          <input
            key={userDetail.id}
            onChange={this.createChangeHandler(userDetail.id)}
            value={userDetail.name}
          />
        ))}
        
        <pre>userDetails: {JSON.stringify(userDetails)}</pre>
        <pre>userDetailsCopy: {JSON.stringify(userDetailsCopy)}</pre>
      </React.Fragment>
    );
  }
}

ReactDOM.render(
  <App />,
  document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>