3

I just start picking up react.js so I went through a lot of tutorials and I've stumbled upon this bit which basically meant to delete an item from the state.

this is how the guy introduced to me the delete function

  delTodo = id => {
    this.setState({
      todos: [...this.state.todos.filter(todo => todo.id !== id)]
    });
  };

Since I am not so familiar with javascript I had a hard time figuring out what the ... operator is doing and why exactly is he using it in the given scenario. So in order to have a better understanding of how it works, I played a bit in the console and I've realised that array = [...array]. But is that true? Is this bit doing the same exact thing as the one from above?

  delTodo = id => {
    this.setState({
      todos: this.state.todos.filter(todo => todo.id !== id)
    });
  };

Could someone more experienced clarify to me why he has chosen to be using that approach instead of the one I've come up with?

Bastian Stein
  • 2,147
  • 1
  • 13
  • 28
Terchila Marian
  • 2,225
  • 3
  • 25
  • 48
  • 4
    The spread operator just simply makes a copy of an existing array in this case. The reason why you'd see it in a lot of React examples is because you don't want to mutate your state directly, rather what you want to do is to make a copy of your state, then make changes to the copy however you want, and finally use the new state/copy with changes to update your component's state. – goto Mar 05 '20 at 17:25
  • so I should stick to his approach? – Terchila Marian Mar 05 '20 at 17:26
  • That's correct. This applies to `objects` too, for example `this.state.someValue = {}` - if you'd want to update `state.someValue` you'd also do `this.setState({ someValue: {...this.state.someValue, foo: "bar" }})` – goto Mar 05 '20 at 17:26
  • basically, what I've understood is that I should always try to avoid changing the state directly? is this right? – Terchila Marian Mar 05 '20 at 17:30
  • 1
    The spread is not the one creating the copy, the filter method is, it returns a new array rather than mutate the one you passed it. You spread it in your example because else you would be saving an array inside another array. – eMontielG Mar 05 '20 at 17:32
  • Yes - if it's an `array` and/or `object`, try to make a copy if possible and applicable. Also, good catch from @eMontielG and the answer below - `filter` will return a new array so you won't be mutating your state directly. You wouldn't need to copy the array in this case, but just keep in mind that in cases where you find yourself mutating the state directly always make a copy. – goto Mar 05 '20 at 17:36
  • what would be the reason why we try not to mutate the state directly? because at this stage It isn't just as obvious – Terchila Marian Mar 05 '20 at 17:38
  • Have a look at my answer. Hopefully it's making it clear. – goto Mar 05 '20 at 17:51

4 Answers4

4

As per the documentation:

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


So, in the example from the tutorial you've mentioned, you wouldn't need to make a copy of the array to update your state.

// GOOD
delTodo = id => {
  this.setState({
    todos: this.state.todos.filter(...)
  })
}

Array.filter method creates a new array and does not mutate the original array, therefore it won't directly mutate your state. Same thing applies to methods such as Array.map or Array.concat.

If your state is an array and you're applying methods that are mutable, you should copy your array.

See more to figure out which Array methods are mutable:

However, if you were to do something like the following:

// BAD
delTodo = id => {
  const todos = this.state.todos
  todos.splice(id, 1)
  this.setState({ todos: todos })
}

Then you'd be mutating your state directly, because Array.splice changes the content of an existing array, rather than returning a new array after deleting the specific item. Therefore, you should copy your array with the spread operator.

// GOOD
delTodo = id => {
  const todos = [...this.state.todos]
  todos.splice(id, 1)
  this.setState({ todos: todos })
}

Similarly with objects, you should apply the same technique.

// BAD
updateFoo = () => {
  const foo = this.state.foo // `foo` is an object {}
  foo.bar = "HelloWorld"
  this.setState({ foo: foo })
}

The above directly mutates your state, so you should make a copy and then update your state.

// GOOD
updateFoo = () => {
  const foo = {...this.state.foo} // `foo` is an object {}
  foo.bar = "HelloWorld"
  this.setState({ foo: foo })
}

Hope this helps.

goto
  • 4,336
  • 15
  • 20
  • Great explanation and use of resources! May I just add, there's a library that takes all the guesswork out of this entire scenario called `immer`, (or hooks = `use-immer`) - 'Winner of the "Breakthrough of the year"': https://www.npmjs.com/package/immer – AveryFreeman Oct 18 '21 at 03:16
3

Why use the spread operator at all?

The spread operator ... is often used for creating shallow copies of arrays or objects. This is especially useful when you aim to avoid mutating values, which is encouraged for different reasons. TLDR; Code with immutable values is much easier to reason about. Long answer here.

Why is the spread operator used so commonly in react?

In react, it is strongly recommended to avoid mutation of this.state and instead call this.setState(newState). Mutating state directly will not trigger a re-render, and may lead to poor UX, unexpected behavior, or even bugs. This is because it may cause the internal state to differ from the state that is being rendered.

To avoid manipulating values, it has become common practice to use the spread operator to create derivatives of objects (or arrays), without mutating the original:

// current state
let initialState = {
    user: "Bastian",
    activeTodo: "do nothing",
    todos: ["do nothing"]
}


function addNewTodo(newTodo) {
    // - first spread state, to copy over the current state and avoid mutation
    // - then set the fields you wish to modify
    this.setState({
        ...this.state,
        activeTodo: newTodo,
        todos: [...this.state.todos, newTodo]
    })
}

// updating state like this...
addNewTodo("go for a run")
// results in the initial state to be replaced by this:
let updatedState = {
    user: "Bastian",
    activeTodo: "go for a run",
    todos: ["do nothing", "go for a run"]
}

Why is the spread operator used in the example?

Probably to avoid accidental state mutation. While Array.filter() does not mutate the original array and is safe to use on react state, there are several other methods which do mutate the original array, and should not be used on state. For example: .push(), .pop(),.splice(). By spreading the array before calling an operation on it, you ensure that you are not mutating state. That being said, I believe the author made a typo and instead was going for this:

 delTodo = id => {
    this.setState({
      todos: [...this.state.todos].filter(todo => todo.id !== id)
    });
  };

If you have a need to use one of the mutating functions, you can choose to use them with spread in the following manner, to avoid mutating state and potentially causing bugs in your application:

// here we mutate the copied array, before we set it as the new state
// note that we spread BEFORE using an array method
this.setState({
      todos: [...this.state.todos].push("new todo")
});

// in this case, you can also avoid mutation alltogether:
this.setState({
      todos: [...this.state.todos, "new todo"]
});
Bastian Stein
  • 2,147
  • 1
  • 13
  • 28
2

As .filter gives you a new array (than mutating the source array), it is acceptable and results in the same behaviour, making spreading redundant here.

What's not acceptable is:

const delIndex = this.state.todos.findIndex(todo => todo.id !== id);
this.state.todos.splice(delIndex, 1); // state mutation

this.setState({
  todos: this.state.todos
});

slice is fine though:

const delIndex = this.state.todos.findIndex(todo => todo.id !== id);

this.setState({
  todos: [
    ...this.state.todos.slice(0, delIndex),
    ...this.state.todos.slice(delIndex + 1)
  ]
});

If you mutate state (which keeps ref same) React may not be able to ascertain which part of your state actually changed and probably construct a tree on next render which is different from expected.

1

The spread operator that the guy's code is applying causes the array returned from the filter function to be copied again.

Since [.filter is returning a new array][https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter], you will already avoid mutating the array in state directly. It seems like the spread operator may be redundant.

One thing I wanted to point out too is while the values in a copied array may be the same as the values in an old array (array = [...array]), the instances change so you wouldn't be able to use '===' or '==' to check for strict equivalency.

const a = ['a', 'b']
const b = [...a]

console.log(a === b) // false
console.log(a == b) // false
console.log(a[0] === b[0]) // true
console.log(a[1] === b[1]) // true

Hope this helps!

Shrubs
  • 41
  • 4