Search code examples
reactjsreact-hookslocal-storagereact-state-management

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

  • 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")  
    }