'Why React state in localStorage is being deleted

I'm having problems to persist the state in the local storage. It's a simple todo app. After adding one todo and refreshing it, all todos are being deleted. Could not figure out why. Here is the relevant code:

const [ todos, setTodos ] = useState([])
      useEffect(() => {                                                                                                         
  │   │   console.log('b1:',JSON.parse(localStorage.todos))                                                                     
  │   │   if (localStorage.todos !==null) {                                                                                     
  │   │   │   console.log(JSON.parse(localStorage.todos))                                                                       
  │   │   │   setTodos(JSON.parse(localStorage.todos))                                                                          
  │   │   }                                                                                                                     
  │   │   console.log('a1:',JSON.parse(localStorage.todos))                                                                     
  │   }, [])                                                                                                                    
  │   useEffect(() => {                                                                                                         
  │   │   // if (todos.length > 0) {                                                                                            
  │   │   │   console.log('b2:',JSON.parse(localStorage.todos))                                                                 
  │   │   │   localStorage.setItem("todos", JSON.stringify(todos))                                                              
  │   │   │   console.log('a2:',JSON.parse(localStorage.todos))                                                                 
  │   │   // }                                                                                                                  
  │   }, [todos])     

console output (b1 before1, a1 after1 etc):

[Log] b1: – [{text: "one", done: false, id: "6c570584b1a"}] (1)
[Log] [{text: "one", done: false, id: "6c570584b1a"}] (1)
[Log] a1: – [{text: "one", done: false, id: "6c570584b1a"}] (1)
[Log] b2: – [{text: "one", done: false, id: "6c570584b1a"}] (1)
[Log] a2: – [] (0)
[Log] b1: – [] (0)
[Log] [] (0)
[Log] a1: – [] (0)
[Log] b2: – [] (0)
[Log] a2: – [] (0)
[Log] b2: – [] (0)
[Log] a2: – [] (0)


Solution 1:[1]

useState hook is asynchronous. According to your code, you call setTodos in the 1st useEffect and then call todos state in the 2nd useEffect that is not sure todos state gets updated.

useEffect will be also triggered for the first time after the component rendering even though it has dependencies (in your case, it's [todos]).

For a possible fix, you should add a condition to check todos state before updating it again in the 2nd useEffect.

useEffect(() => {                                                                                                         
        console.log('b1:',JSON.parse(localStorage.todos))                                                                     
        if (localStorage.todos !==null) {                                                                                     
           console.log(JSON.parse(localStorage.todos))                                                                       
           setTodos(JSON.parse(localStorage.todos))                                                                          
        }                                                                                                                     
        console.log('a1:',JSON.parse(localStorage.todos))                                                                     
     }, [])                                                                                                                    
     useEffect(() => {                                                                                                         
        if (todos && todos.length > 0) {                                                                                            
           console.log('b2:',JSON.parse(localStorage.todos))                                                                 
           localStorage.setItem("todos", JSON.stringify(todos))                                                              
           console.log('a2:',JSON.parse(localStorage.todos))                                                                 
        }                                                                                                                  
     }, [todos])  

Those side-effects you implemented are not suitable for delete-all case. So I'd propose 2 solutions

The 1st solution is using another state to track the first load and next loads (like calling APIs)

const [isLoaded, setIsLoaded] = useState(false)
useEffect(() => {                                                                                                         
        console.log('b1:',JSON.parse(localStorage.todos))                                                                     
        if (localStorage.todos !==null) {                                                                                     
           console.log(JSON.parse(localStorage.todos))                                                                       
           setTodos(JSON.parse(localStorage.todos)) 
           setIsLoaded(true)                                                                         
        }                                                                                                                     
        console.log('a1:',JSON.parse(localStorage.todos))                                                                     
     }, [])                                                                                                                    
     useEffect(() => {                                                                                                             
           if(isLoaded) {
              console.log('b2:',JSON.parse(localStorage.todos))                                                                 
              localStorage.setItem("todos", JSON.stringify(todos))                                                              
              console.log('a2:',JSON.parse(localStorage.todos)) 
           }                                                                                             
     }, [todos, isLoaded])  

The 2nd solution is updating localStorage in deleteAll instead

useEffect(() => {                                                                                                         
        console.log('b1:',JSON.parse(localStorage.todos))                                                                     
        if (localStorage.todos !==null) {                                                                                     
           console.log(JSON.parse(localStorage.todos))                                                                       
           setTodos(JSON.parse(localStorage.todos))                                                                          
        }                                                                                                                     
        console.log('a1:',JSON.parse(localStorage.todos))                                                                     
     }, [])                                                                                                                    
     useEffect(() => {                                                                                                         
        if (todos && todos.length > 0) {                                                                                            
           console.log('b2:',JSON.parse(localStorage.todos))                                                                 
           localStorage.setItem("todos", JSON.stringify(todos))                                                              
           console.log('a2:',JSON.parse(localStorage.todos))                                                                 
        }                                                                                                                  
     }, [todos])  

function deleteAll() {
   setTodos(null)
   localStorage.removeItem("todos")  
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1