React Hooks showing inconsistent behaviour

Viewed 88

I wanted to get a feel for react hooks and was trying them out by making a normal todo list with adding and removing capabilities but I have come across an annoying behaviour and I do not know the logic behind it.

 const handleDeleteTodos = (event) => {
    todos.splice(event.target.name, 1);
    setTodos(todos)
    window.localStorage.setItem('Todos', JSON.stringify(todos));
  };

So the code above does delete the element, I know this because when I reload the page, the element is gone. But it doesn't re-render like the add todo function. While the code below works.

const handleDeleteTodos = (event) => {
    const newTodos = [...todos];
    newTodos.splice(event.target.name, 1); 
    setTodos(newTodos)
    window.localStorage.setItem('Todos', JSON.stringify(newTodos));
  };

I only difference I see is the object destructuring aspect of the code.

I just wanted to know what is the logic behind this behaviour.

3 Answers

You're seeing stale props.

Use the function form of setState to modify objects without that caveat (since the reference to that object is passed to you by React).

const handleDeleteTodos = (event) => {
  const { name } = event.target; // important to copy the name before `setTodos`, since react reuses events
  setTodos((todos) => {
    const newTodos = [...todos];
    newTodos.splice(name, 1);
    return newTodos;
  });
};

Also, use useEffect to modify localStorage based on todos, since it literally is a side effect of state being modified, instead of doing it manually in each modification function.

useEffect(() => {
  window.localStorage.setItem('Todos', JSON.stringify(todos));
}, [todos]);

React only rerenders if the reference to the object on state has changed (or if it's scalar, the value itself). You must create a new object and set it on state.

The first example uses the same reference, so React has no idea anything changed. The second example uses a new reference.

Issue

Array.prototype.splice does an in-place mutation of the array.

Array.prototype.splice

The splice() method changes the contents of an array by removing or replacing existing elements and/or adding new elements in place. To access part of an array without modifying it, see slice()

You are mutating the current state and then saving the same reference back into state.

const handleDeleteTodos = (event) => {
  todos.splice(event.target.name, 1); // <-- mutation
  setTodos(todos); // <-- same state reference
  window.localStorage.setItem('Todos', JSON.stringify(todos));
};

Because a new array reference is never created, React doesn't see that state was ever updated.

Solution

Creating a new array reference allows React to use a shallow reference equality check and rerender.

const handleDeleteTodos = (event) => {
  const newTodos = [...todos]; // <-- shallow copy into new reference
  newTodos.splice(event.target.name, 1); // <-- mutate copy
  setTodos(newTodos); // <-- save new reference into state

  window.localStorage.setItem('Todos', JSON.stringify(newTodos));
};

A more conventional method is to use Array.prototype.filter to shallow copy the array and remove the element at the same time.

const handleDeleteTodos = (event) => {
  const newTodos = todos.filter((_, index) => event.target.name !== index);

  setTodos(newTodos);

  window.localStorage.setItem('Todos', JSON.stringify(newTodos));
};
Related