alanbuchanan alanbuchanan - 21 days ago 6
Javascript Question

Avoiding `setTimeout` 'hack' to ensure state is in sync

I wrote a small todo app using React, in which the user can add and remove items to their list.

As a bonus feature, the list is saved to

localStorage
as it's mutated by the user, and used on the initial mount of the component.

In both the
addTodo
and
removeTodo
methods, the pattern is this:


  1. Set the new state by adding or removing an item of
    this.state.todos

  2. Copy the state to
    localStorage



For example, here is the
removeTodo
method:

removeTodo(indexOfItemToRemove) {
const todosCopy = this.state.todos.slice();

todosCopy.splice(indexOfItemToRemove, 1);

this.setState({
todos: todosCopy
});

this.updateLocalStorageWithState();
}


Here is what I am currently doing to save
this.state.todos
to
localStorage


updateLocalStorageWithState() {
setTimeout(() => {
localStorage.setItem('localStorageTodos', JSON.stringify(this.state.todos));
}, 1);
}


I found that without using
setTimeout
, the
localStorage
was always 1 step behind, using what the user might perceive as an outdated version of the list.

This feels like a hack. How can I incorporate a way of doing the same thing without having to use a
setTimeout
hack?

Answer

setState() is not necessarily executed synchronuosly. This causes the behavior you are experiencing:

setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.

There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.

Source


Luckily, setState() takes a second parameter, which is a callback that is called when the state update has been completed. You can use that to synchronize your local storage entry:

this.setState({
    todos: todosCopy
}, () => {
    localStorage.setItem('localStorageTodos', JSON.stringify(this.state.todos));
});

Alternatively, why not set the local storage entry directly instead of reading from state first?

this.setState({
    todos: todosCopy
});

localStorage.setItem('localStorageTodos', JSON.stringify(todosCopy));