Reasons not to mutate React state

Hey everyone, this topic came up when I was chatting with a coworker who was less familiar with React, and I was hoping someone could shed some light on what is probably a very basic question. If I have some piece of state, say for example const [person, setPerson] = useState({ name: 'Ed', age: 99 });, obviously if I were to mutate the object directly, it would not trigger any update and we'd be left with stale state. However, if an update WERE to occur in lockstep with this mutation, would that have negative implications beyond simply not being a best practice? For example...
const [person, setPerson] = useState({ name: 'Ed', age: 99 });
const [_, forceUpdate] = useState();

const change = () => {
person.name = 'The Cooler Ed';
forceUpdate();
};
const [person, setPerson] = useState({ name: 'Ed', age: 99 });
const [_, forceUpdate] = useState();

const change = () => {
person.name = 'The Cooler Ed';
forceUpdate();
};
Obviously this is a contrived example, but you can see the point. The coworker in question is newer to React, and was mutating state but then another update was causing an update, so he hadn't realized it was an issue. I explained that this isn't good because it may introduce a bug if the code changes in the future, but I couldn't exactly describe whether or not there are any issues with changing prior state BEYOND this. Would it introduce issues? Or is it simply a matter of an update not occurring, which I've already described?
5 Replies
Unknown User
Unknown User•16mo ago
Message Not Public
Sign In & Join Server To View
nexxel
nexxel•16mo ago
reasons not to mutate state: chatgpt summed it up pretty well Predictable component updates: React relies on comparing the current state with the new state to determine what parts of the UI need to be updated. If state is mutated directly, React may not be able to detect the changes, leading to unpredictable updates. Avoiding side effects: Mutating state directly can lead to unexpected side effects and bugs. For example, if you have multiple components that depend on the same state, directly mutating that state in one component could cause unintended changes in the other components. Performance optimizations: React may batch state updates for performance reasons, but it can only do so if you use the setState function. Directly mutating state can prevent React from optimizing updates and lead to performance issues. Encouraging immutability: By using setState instead of mutating state directly, you are encouraged to think in terms of immutable data structures. This can help make your code more predictable and easier to reason about.
Roren
Roren•16mo ago
I understand that this is the case - my question was more so intended to present what the person was doing in effort to mutate state, and ask about potential pitfalls that might be beyond my understanding. I appreciate you clarifying that point, though! That all makes total sense. It seems like my understanding was correct, in that for the most part it's more so about what COULD happen as a result of unexpectedly updating state incorrectly, rather than an additional concrete consequence that WILL happen. Thank you very much for the responses, I really appreciate it. Just to tack on here, in this situation it might be worth updating state with a callback, rather than using the value returned from useState. You could setPerson((prev) => ({ ...prev, name: 'The Cooler Ed' })) instead, and that would ensure you're always referring to the most recent state value when the update occurs, rather than a potentially stale value
b_e_n_t_e_n
b_e_n_t_e_n•16mo ago
Heh this is a funny one React under the hood might throw away the mutated state depending on the update that triggered a render.
Roren
Roren•16mo ago
Oh really? That's interesting! Do you mean, like, if I were to do something like...
const [user, setUser] = useState({ loginCount: 0 });

const mutate = () => {
user.loginCount++;
};

const update = () => {
setUser((prev) => ({
...prev,
loginCount: prev.loginCount + 1, // React doesn't track inappropriate mutations?
}));
};

const changeCount = () => {
mutate();
update(); // Even if mutate has run, update's setter still picks up 0 from prev?
};
const [user, setUser] = useState({ loginCount: 0 });

const mutate = () => {
user.loginCount++;
};

const update = () => {
setUser((prev) => ({
...prev,
loginCount: prev.loginCount + 1, // React doesn't track inappropriate mutations?
}));
};

const changeCount = () => {
mutate();
update(); // Even if mutate has run, update's setter still picks up 0 from prev?
};
That's what springs to mind, but if there's anything else I can read please let me know, thank you very much 🙂