2

So, I'm just going through a React course. Learning this framework for the first time so this is likely a dumb question. Here's code I've been given for updating a property on an object stored in state as an array of objects.

const [squares, setSquares] = React.useState(boxes)

function toggle(clickedSquare) {
    setSquares(prevSquares => {
        return prevSquares.map((square) => {
            return square === clickedSquare ? {...square, on: !square.on} : square
        })
    })
}

...but, the following code I wrote works too and seems simpler, what's wrong with this approach? State values are only shallow immutable. Objects stored in a state array are themselves mutable, as far as I can tell...

const [squares, setSquares] = React.useState(boxes)

function toggle(clickedSquare) {
    clickedSquare.on = !clickedSquare.on;
    setSquares(prevSquares => [...prevSquares])
}

Also consider this example where I have an array containing deeply nested objects held in state.

[
    {
        trunk: {
            limb: {
                branch: {
                    twig: {
                        leaf: {
                            color: "green"
                        }
                    }
                }
            }
        }
    }
]

I want to change that "green" to "brown". It states in this article Handling State in React that...

  1. Deep cloning is expensive
  2. Deep cloning is typically wasteful (instead, only clone what has actually changed)
  3. Deep cloning causes unnecessary renders since React thinks everything has changed when in fact perhaps only a specific child object has changed.

The thing that has changed in the tree example is just the leaf object. So only that needs to be cloned, not the array and not the whole tree or trunk object. This makes a lot more sense to me. Does anyone disagree?

This still leaves (no pun intended) the question of what bugs can be introduced by updating property values in a state array my way and not cloning the object that has the change? A single concrete example would be very nice just so I can understand better where I can optimize for performance.

2 Answers2

2

clickedSquare.on = !clickedSquare.on; is a state mutation. Don't mutate React state.

The reason the following code is likely working is because it has shallow copied the squares state array which triggers a rerender and exposes the mutated array elements.

function toggle(clickedSquare) {
  clickedSquare.on = !clickedSquare.on;        // <-- mutation!
  setSquares(prevSquares => [...prevSquares]); // new array for Reconciliation
}

It may not have any adverse effects in this specific scenario, but mutating state is a good foot gun and likely to cause potentially difficult bugs to debug/diagnose, especially if their effects aren't seen until several children deeper in the ReactTree.

Just always use the first method and apply the Immutable Update pattern. When updating any part of React state, even nested state, new array and object references need to be created for React's Reconciliation process to work correctly.

function toggle(clickedSquare) {
  setSquares(prevSquares => prevSquares.map((square) => // <-- new array
    square === clickedSquare
      ? { ...square, on: !square.on } // <-- new object
      : square
  ));
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • OK, I think I get it. So in the guts of React, sometimes (but not always) your objects in state are overwritten with new ones holding the values it had the last time the useState or set functions were called. So any changes you make outside of those functions might but also might not be persisted. Is that about right? – mechafractal Apr 30 '22 at 10:28
  • You said "new array and object references need to be created for React's Reconciliation process to work correctly" but in the code shown it isn't creating new object references for anything except the object being changed. So why does this not cause problems with the reconciliation process if it relies on always having new object references? – mechafractal May 01 '22 at 20:17
  • @mechafractal React uses shallow reference equality. `Array.prototype.map` returns a ***new*** array reference, and `{ ...square }` creates a ***new*** object reference for the specific element being updated. The other elements that aren't being updated don't need new references, they are simply passed through into the new array. Does this make sense? – Drew Reese May 02 '22 at 20:53
  • Yes that makes sense. But that goes against what the Handling State in React article says about deep cloning not being necessary. Does it check reference values all the way down or just one level deep? Do we know or is React just a black box in that regard? – mechafractal May 04 '22 at 06:10
  • @mechafractal What "Handling State in React" article are you referring to? Deep cloning isn't necessary, and in fact could lead to unnecessary rerenders of component consuming state that *didn't actually* update. Give this [Immutable Update Patterns](https://redux.js.org/usage/structuring-reducers/immutable-update-patterns) doc a read. It's a Redux doc, but it is highly applicable to React state updates of complex objects. To answer your question, no, React doesn't do a deep dive, that is what a shallow reference equality check is, if the reference is the same, it's assumed the state is equal. – Drew Reese May 04 '22 at 06:14
  • I’m referring to the article in the second half of my question above [Handling State in React](https://www.freecodecamp.org/news/handling-state-in-react-four-immutable-approaches-to-consider-d1f5c00249d5/). Thanks for the article, I’ll give it a read but it sounds on the face of it like React checks the references of objects and arrays (which are objects) and the references of child objects and arrays but doesn’t check any value types held in properties. If this is the case then it’s the detail I was after. Maybe the article you sent will clear things up even more. – mechafractal May 05 '22 at 13:33
  • Oh no, so you really do need to clone the ENTIRE object no matter how small or how deep your change is. I’m just going to use React’s state management as a last resort I think and where I do use it, keep objects flat with no nested objects at all. Thank for you help, I now understand! Took a while for the penny to drop I know. Thanks for your patience. Still a bit fuzzy on why my example works and what bugs it can introduce but I’m past caring. – mechafractal May 05 '22 at 14:05
  • The same answer was asked by someone else. Some good responses here too. https://stackoverflow.com/questions/37755997/why-cant-i-directly-modify-a-components-state-really – mechafractal May 05 '22 at 14:47
0

Here, You are using map() method which return only true or false for that condition. So, You should use filter() method instead of map() method which return filtered data for that condition.

For Example:

const arr = [
    {
        name: 'yes',
        age: 45
    },
    {
        nmae: 'no',
        age: 15
    }
]

const filterByMap = arr.map(elm => elm.age > 18)
console.log(filterByMap) // outputs --> [ true, false ]

const filterByFilter = arr.filter(elm => elm.age > 18)
console.log(filterByFilter) // outputs --> [ { name: 'yes', age: 45 } ]
Jay Patel
  • 218
  • 2
  • 8
  • The map method in my example is returning whole objects, not booleans. I see filter as being a function to use when I only want a subset of the array back. In this case I'm trying to update one property on one object in an array of objects. React makes me give a new array and a new object in place of the one that was changed. I'm just trying to understand why and how far to take this concept. If the array and the objects within it are huge, do they need to be cloned in their entirety to keep React happy even if only one property 20 levels deep needs to be changed? – mechafractal Apr 30 '22 at 10:56
  • Okay, I'm understood that. Please watch the following video (https://youtu.be/-3lL8oyev9w). You will understand why we should also make of array or object while updating the property of state. – Jay Patel May 01 '22 at 11:43
  • That video doesn't answer my question. That simply deals with how to update properties directly on a single object held in state. It doesn't mention arrays. I'm not questioning the use of { ...square, on: !square.on } if that object is the one being held in state because if you don't you don't get the intended behavior. However unlike the example in that video, my code works exactly as intended. It updates the object's property leaving the rest of the object and its values intact while also triggering React to update ReactDOM with refreshed versions of the components. So what's wrong with it? – mechafractal May 01 '22 at 20:10
  • `Array.prototype.map` callback isn't returning booleans, it is returning object values, so your explanation doesn't make any sense. `.map` is used to return a 1-to-1 mapping of the original array, i.e. an array of equal length. `.filter` isn't applicable here. – Drew Reese May 02 '22 at 20:59