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>