4

Background

I've been playing around with React and made a simple todo list app to explore some concepts. While testing my code, I realized that returning a new state from setState() doesn't seem to be necessary when mutating/updating a state stored collection.

According to several other StackOverflow questions and the React documentation, the code I wrote should either not work, or produce unexpected results. However, in all of my testing, the code does seem to work and is more readable and concise than the suggested methods for mutating collections.

Why does my code work, and what, if any, is the danger of using it as a pattern?

I've created two versions of my app. One which uses a method for updating collections as mentioned in this question, and another which has setState() return an empty object instead. I've uploaded full versions of the app to CodePen so you can see that they behave identically.

Code Segments

Here is the key code segment from the original. I'm using a callback for setState() as a way to guarantee that the state read and state write happen automatically (in case of asynchronous state changes between calls).

addTask(taskText) {
        this.setState(prevState => {
            const updatedTasks = prevState.tasks;
            updatedTasks.set(prevState.uid, taskText);   // use the original, un-incremented uid
            return {
                uid: ++prevState.uid,   // increment prev uid and store it as new state
                tasks: updatedTasks, // set tasks to updated tasks
            }
        }, this.logState);
    }


However, it seems that returning the actual changed state is unnecessary for collections (or any other reference-value object). As the following code also works. The only differences are that I hoisted the uid to a class property and didn't bother to return the updated map.

addTask(taskText) {
    this.setState(prevState => {
        const updatedTasks = prevState.tasks;
        updatedTasks.set(this.uid, taskText);   // use the original, un-incremented uid
        this.uid++;
        return {
            // return an empty object
            //uid: ++prevState.uid,   // update uid to one more than prev uid
            //tasks: updatedTasks, // set tasks to updated tasks
        }
    }, this.logState);
}


If I don't care about wrapping the mutation inside of an arrow function as an asynchrounous guard, then the method can be simplified even further:

addTask(taskText) {
        this.state.tasks.set(this.uid, taskText);   // use the original, un-incremented uid
        this.uid++;
        this.setState({});
}


It seems that the only thing that really matters is the setState() call itself. My initial understanding was that React was pretty efficient, and onyl checks for state changes that it's told about. However, the last example seems to indicate setState() causes React to examine all of its known state for any changes (regardless of how they got there), and then update/render accordingly.

Changes to primitives would still need to be wrapped inside the returned setState() object, but for objects (or at least Maps), what is the downside to eliminating the complexity and just using a naked setState() call?


Reference

I'm using the following:

  • React and ReactDOM v16.2.0 (15.1.0 on the CodePens)
  • Babel 6.26.0 (with stage-2 support for class properties)
  • ECMAScript 6
Brian H.
  • 2,092
  • 19
  • 39

1 Answers1

1

1) React setState is smart to identify the changes that has happened to the state object entities. So if you are thinking that this.setState({}) will make this.state = {} as empty object then it is wrong. What it will do is compare the changes of the objects entities that has been made and it will update only those changes.

2) According to the react docs :

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

So this means that you can mutate state if you have map values using set function so it won't give you error but if you try to initialize it like this this.state.tasks = [] then surely it will give error. So your expectation of getting error will not result because it this.state is mutable but we don't need to change it directly. Also, sometimes it may set the improper values on the later this.setState calls.

3) So, if you set state state using set function of Map then the state will be manipulated but the page won't re-render as it should re-render on update. So you should not mutate the state like this rather you should use this.setState function to avoid unnecessary things to happen.

4) Incase of removeTasks you are using the prevState callback of this.setState so what react do is it will use the prevState and if you return blank object then it will initialize the whole object to its initial state that you have defined. So, if you want to try you can set initial state as this.state = {tasks: new Map().set(1, "awdawd")} and then see you will get unexpected results when clicked on the remove tasks button. So, react returns initial state if you return an empty object.

Just for reference, you can read more details about setState in the below link: faq-state

PassionInfinite
  • 640
  • 4
  • 14
  • I think this is a good answer. The lines `const updatedTasks = prevState.tasks; updatedTasks.set(this.uid, taskText);` have updated the component state, then once the `setState` call has completed, React re-renders. It works because the call to `setState` exists. – Matt Holland Mar 21 '18 at 00:14
  • yea, if you don't have that `this.setState` then react won't re-render and you will have mismatched states. @MattHolland – PassionInfinite Mar 21 '18 at 00:17
  • To clarify a few points. I apologize if I implied a belief that `setState({})` would overwrite state. That was not the case. `setState()` merges the supplied state with the existing state. In regards to 3), I think you are incorrect. If I add `componentWillMount() {this.state.tasks.set(0, 'abc'); this.uid++; }` the app runs correctly. The first entry renders and I can add/delete freely. I know this is not recommended, but in this case, works. Finally, while I appreciate the explanation, it did not really answer the question, which is what are the dangers of using the shortened pattern? – Brian H. Mar 21 '18 at 03:23
  • Shortened patterns?? – PassionInfinite Mar 21 '18 at 06:08
  • If by "shortened pattern" you mean calling `this.state.foo = bar` the main danger is that a subsequent call to `setState()` can overwrite that change and you will have inconsistencies. If you need to keep state that does not re-render the component, keep it somewhere else. – Matt Holland Mar 21 '18 at 17:32
  • @MattHolland By "shortened pattern", I'm referring to mutating state by modifying a collection, *not* by overwriting a state variable. Specifically, updating a map or dictionary as in `this.state.someCollection.set('foo', 'bar'); this.setState({})` instead of `let collectionReference = this.state.someCollection; collectionReference.set('foo', 'bar'); this.setState({someCollection: collectionReference});` One version is much shorter and cleaner than the other, but I never see it being used. Are you saying that a subsequent call to setState() could modify the contents of a collection? – Brian H. Mar 21 '18 at 18:12
  • That's the general concern, yes. As you're calling `setState` in the very next line it might be OK but I would not like to rely on that, personally. tbh I tend not to use plain JS Sets (or Maps) in state. Really state should be immutable and the collections are optimised for mutability (Also they aren't serializable which has pitfalls with Redux for example). I would probably use something like Immutable.js instead. Just my opinion though... :) – Matt Holland Mar 21 '18 at 19:20