Is it always wrong to mutate state?

Let me start off by saying I know you shouldn't mutate state because your state and view can become out of sync and it is difficult to find issues when you can't follow the flow of data. The answer to most state problems is to move state up to the parent.

But it seems more sensible to keep state related to a component, in the component. For example, using controlled inputs in a form just to have access to the values on submission seems to add unnecessary boilerplate. Your form reducer needs case statements for all input values and the form component has to pass the value and dispatch to the input. Yes, you can use a ref, new FormData(event.target) in onSubmit, or even some iframe magic to get around controlled inputs but most people say to just lift your state up and use controlled inputs. I wish there was a better way.

I was thinking the form can pass an object with initial state to the child and the child could mutate it. Just to be clear, the the mutated state would not be used for updating any part of the view. The mutated state would only be used to sync the parent's state with the child's state. This way, the parent would not need to control the inputs and still be able to reference the input values.

Here is a codesandbox example to illustrate what I'm talking about.

To be clear, I'm not saying state should never be moved up, there are definitely times when it makes sense. It's not clear to me why it is common to hear "all state mutation is bad".

Posted Jun 8, 2019, 1:20 AM UTC · edited

39
acemarke•

Your post sorta seems to be describing two different things. "Lifting state up" and dealing with a controlled inputs is a very different thing than "updating state immutably or mutating it".

But yes, mutating state is bad, 99% of the time. It's only going to be more of a problem going forward. For example, while this.setState() in class components doesn't care if you mutate or not, useState() setter functions do. If you pass the same reference into a useState() setter, or return the same reference from a useReducer() setter, React will bail out of that update. At the same time, if you don't call those, React won't know to re-render.

Also, not only does mutation cause problems for all of the common re-render performance optimizations (shouldComponentUpdate, PureComponent, and React.memo()`), once Concurrent React comes into play React will commonly be starting to re-render and then throwing away partial render results. If you're mutating somewhere during this process, stuff will break.

7
ProfessorTagOP•

Thanks for the information. I guess I'm not really asking if mutating state is bad, but mutating an object used for initializing state, that the parent doesn't use for rendering. If I make sure to mutate in useLayoutEffect, I'm hoping it won't be an issue with concurrent mode.

1
acemarke•

Is the only reason you're considering this to try to avoid having controlled state in a parent? That seems like a questionable approach.

If you really don't want to deal with controlled inputs, then use uncontrolled inputs in the child component, have the parent pass down a callback that should be used when the form is submitted, and have the child grab the data from the form in some way and pass it to the parent.

Also consider using a form library like Formik or React Final Form if you feel that dealing with forms is annoying.

But, all that said, controlled inputs is the "React way" to deal with forms, and it's definitely useful for more than just "having data on submission".

3
ProfessorTagOP•

Dealing with nested state inspired this and I noticed not needing controlled inputs as a side effect. I agree, controlled inputs enable much more than "having data on submission". They need to be controlled any time an input effects some other part of the view. Thanks for this discussion, I'm not going to go ahead with this pattern. There are established ways to solve these problems.

1
Herm_af•

Mutating state in react is bad because setState and useState tell react to rerender.

Mutating stuff isn't bad in general.

1
hatch7778•

Your example works only because you are re-rendering everything where it shouldn't be needed - if you did this properly (so the ListItems are not re-rendered if they are not changed) mutating state wouldn't work.

In general - if you can get away with state mutation, you're probably doing something else wrong. Or maybe you should put those mutations as local variable instead, since you don't need to subscribe to changes anyway.

1
ProfessorTagOP•

I don't see what you're seeing. Where am I re-rendering anything unnecessarily?

1
hatch7778•

Note that with paint-flashing you can't see this because react uses virtual dom and it does diffs before pushing to dom.

Add console.log to render of ListItem and you will see that virtual dom is being re-rendered for all items when you add an item.

1
ProfessorTagOP•

My example still works without re-rendering. I added React.memo to the ListItem and it still works.

1