Adam
Adam

Reputation: 43

React state is changing unexpectedly

I have a list (news), it comes from the server and each object has multiple fields that the user can edit. I'm saving the list in a state called originalData, and I'm saving the first news item in a state called activeNews:

componentDidMount(){
    fetch('/api/published/news_published_english').then(res => res.json()).then(news =>{
        if(news && news.rows[0]){
            const new_news = [...news.rows];
            this.setState({originalData:[...news.rows]});
            this.setState({activeNews:new_news[0].doc.content});
        }
    });
}

When the user clicks on another news item, activeNews will change to the selected index. handleChange takes the index the of the field that the user want to change within the active news item and its value:

handleChange(index, value) {  
    const x = [...this.state.news];
    x[index].value = value;
    this.setState({ news: x });

    console.log("original", this.state.originalData);
    console.log("news", news);
}

Now the problem is why originalData keeps updating with the values that the user changes? I want originalData to not change but it keeps changing in the handleChange function!

This is the only place where I called setState on originalData but it keeps changing to the value that is getting passed to handleChange.

this.setState({ originalData: [...news.rows] });

Upvotes: 1

Views: 1142

Answers (3)

Hinrich
Hinrich

Reputation: 14023

In your code, x[index].value = value is modifying the original array, which is why it is broken. You could write your change handler like this:

handleChange(index, value) {
    this.setState(state => (
        { news: state.news.map((item, i) => i === index
            ? { ...item, value } 
            : item
        }
    ));
}

Note that I am using a callback function in setState, this is recommended when your new state is dependend on previous state.

EDIT Actually, there is no need to map over the whole array, because you already know the index. Look at caesay's answer which is better performancewise:

 handleChange(index, value) {
    this.setState(state => {
        const newsCopy = [ ...state.news ];
        newsCopy [index] = { ...arr[index], value };
        return { news: newsCopy };
    });
}

Upvotes: 1

caesay
caesay

Reputation: 17233

You're mutating the original objects in your handleChange function when you do this:

x[index].value = value;

To better show what's happening, I'm going to expand your code a bit

handleChange(index, value) {  
    const copiedArray = [...this.state.news];
    const originalObject = copiedArray[index];
    originalObject.value = value;
    this.setState({ news: copiedArray });
}

As I've eluded to with the variable names here, even though you've created a new array - the object at this.state.news[index] and copiedArray[index] are the same object - so changing .value will change in both places. In order to solve this, you need to do a deep clone. You can do this by finding a library (like lodash) which can do this, or you can take a quicker solution and change your code as follows:

handleChange(index, value) {  
    const copiedArray = [...this.state.news];
    const originalObject = copiedArray[index];
    const copiedObject = Object.assign({}, originalObject);
    copiedObject.value = value;
    copiedArray[index] = copiedObject;
    this.setState({ news: copiedArray });
}

Or a condensed version of the same thing:

handleChange(index, value) {  
    const arr = [...this.state.news];
    arr[index] = { ...arr[index], value };
    this.setState({ news: arr });
}

Upvotes: 3

deowk
deowk

Reputation: 4318

This is a common mistake in react apps, you are creating a copy of the array this.state.news which is an array of objects.

When you do x[index].value = value you are actually modifying the original object that is still being referenced in this.state.news.

This --> const x = [...this.state.news]; makes a copy of the array but not the objects, they are still referencing the original objects.

What you need to be doing is something like this:

const news = this.state.news.map((x, i) => {
  if (i === index) {
    return {...x, value};
  } else return x
})
this.setState({ news })

Upvotes: 2

Related Questions