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.

0

2 Answers 2

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
  ));
}
Sign up to request clarification or add additional context in comments.

8 Comments

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?
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 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?
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 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 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.
|
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 } ]

4 Comments

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?
Okay, I'm understood that. Please watch the following video (youtu.be/-3lL8oyev9w). You will understand why we should also make of array or object while updating the property of state.
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?
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.