0

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)
Kolom
  • 217
  • 1
  • 11
  • Does this answer your question? [The useState set method is not reflecting a change immediately](https://stackoverflow.com/questions/54069253/the-usestate-set-method-is-not-reflecting-a-change-immediately) – Martin May 06 '22 at 05:38
  • @Martin I read the question. It's mostly related to console logged values being stale or closured values. In my case I'm having problem with reading from/writing to localStorage. I could not figure out the relation. Maybe it's me. But I should say it has some great insight and links to some good articles. – Kolom May 06 '22 at 05:58
  • What is `localStorage.todos`? Are you not using `window.localStorage`? – Drew Reese May 06 '22 at 07:11
  • @Kolom You are overwriting your own `localStorage.todos` value with the initially empty array value of `todos` with this line here: `localStorage.setItem("todos", JSON.stringify(todos))`. You mistakenly assume your first `useEffect` will have set the todos value by then, but this is not the case. The second `useEffect` still references the initial empty array value at the point in time when it is first executed: after the first rendering. – Martin May 06 '22 at 08:46
  • @DrewReese The code uses `window.localStorage`. It is a global value therefore you can omit `window.` and just use `localStorage`. And `localStorage.todos` is a shorthand for `localStorage.getItem('todos')` – Martin May 06 '22 at 08:52
  • 1
    @Kolom If you want to initialize local state with a value taken from localStorage just use the initializer syntax for `useState` without any `useEffect`. This would look like this: `const [todos, setTodos] = useState(()=>JSON.parse(localStorage.todos || '[]'));` – Martin May 06 '22 at 08:57
  • @Martin `() => JSON.parse(localStorage.todos || '[]')` returns a function which is not values for state, I guess so? – Nick Vu May 06 '22 at 09:07
  • @NickVu The function is correct and intentional: [docs](https://reactjs.org/docs/hooks-reference.html#lazy-initial-state) – Martin May 06 '22 at 09:18
  • Oh interesting! tdil. It's possibly the case for why the OP does not get `localStorage` data. I'll try to align the answer for this too! Nice idea! @Martin – Nick Vu May 06 '22 at 09:20
  • @Martin Thanks. I know that, but wanted OP to confirm what they are actually using since the localStorage shorthand is a bit unorthodox. I agree with you that OP is mutating localStorage before the state is updated. – Drew Reese May 06 '22 at 15:57
  • @Martin thanks for the help. I tried what you recommend for lazy loading but I got "ReferenceError: localStorage is not defined" error. I guess since there is no rendering initially you can not access localstorage. So I'm back at square one in that section, just using an empty array for useState. – Kolom May 07 '22 at 12:24
  • @Martin, can you check my other question about the same app, if you have time. Thanks. https://stackoverflow.com/questions/72152709/react-array-filtering-is-not-working-as-expected – Kolom May 07 '22 at 12:56

1 Answers1

0

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")  
}
Nick Vu
  • 14,512
  • 4
  • 21
  • 31
  • You can check my updated answer, @Kolom. I'd propose that you should use only 1 `useEffect` but handling all `todos` changes – Nick Vu May 06 '22 at 05:41
  • Thanks for the answer. Default value didn't work because the localstorage is not reachable at that moment and returns undefined. I thought about putting condition on second useEffect, however I have another function called "deleteall" which deletes all todos. In that case the delete all IS deleting todos from display but not from localstorage. Do you have any ideas about that? – Kolom May 06 '22 at 05:43
  • 1
    I updated 2 possible solutions for your case, I guess it's suitable for your situation due to `localStorage` population problem @Kolom – Nick Vu May 06 '22 at 05:53
  • Is it helpful for your case? Anything else you're struggling with? @Kolom – Nick Vu May 06 '22 at 07:07
  • Thanks for the help. It solved my problem already in that section. Really appreciate you help. Don't know why people downvote your answer. I'm having another problem in the same app regarding filtering an array but I think it's more about Material UI than React. I'll post the question today. Because I couldn't find a solution. @Nick Vu – Kolom May 07 '22 at 12:27
  • And for lazy loading section, it still don't work as described. I mentioned that in comment under the question. Refer that please. If you agree with me you can delete that section. I going to wait a little bit more and if we don't get a more comprehensive answer than yours, which I don't expect, I will mark it as the answer. Thanks again for your time. – Kolom May 07 '22 at 12:35
  • Hmm I haven't tried it locally, I'll get back to you after the testing, but could I know you're using any server-side rendering framework? @Kolom – Nick Vu May 07 '22 at 12:48
  • It's a next app, indeed. – Kolom May 07 '22 at 12:54
  • And here is the link for my other question: https://stackoverflow.com/questions/72152709/react-array-filtering-is-not-working-as-expected – Kolom May 07 '22 at 12:54
  • Ohhh if it's Next.js, I have a similar answer here https://stackoverflow.com/a/71359449/9201587, but I guess you don't want to use cookies. If you want to use `localStorage`, you only can access it from the client-side (like what you're doing right now with `useEffect` or with `componentDidMount`) @Kolom – Nick Vu May 07 '22 at 14:06
  • Ah ok I see what you mean. I just wanted to use localStorage for initial phase until I decided if it's worth to transfer to mongodb. @Nick Vu – Kolom May 07 '22 at 14:11
  • by the way, I removed the initial state part in my answer to make it clear as you suggested, and I'm checking your 2nd question now. Hopefully, I can find something useful for your case :D @Kolom – Nick Vu May 07 '22 at 14:15
  • If you don't have any further doubt, could you please mark my answer as a solution, that will give me a little support in gaining more confidence to help people? Thank you in advance! :D @Kolom – Nick Vu May 07 '22 at 14:31
  • yeah sure. Thanks Nick, you are really awesome. – Kolom May 07 '22 at 14:33
  • If you have any other questions or something you're struggling with, you can reach out to me anytime here @Kolom – Nick Vu May 07 '22 at 14:34
  • That's awesome. @Nick Vu. – Kolom May 07 '22 at 14:38