My First Dev.to Post — React TODO App with LocalStorage
I was brushing up on my React knowledge and thought of making a small project. I decided to make a TODO app (everyone makes it when they start, I think 😄), and started by building the base app without localStorage.
It worked, but with every refresh, the list state resets and you’re left with no data. For persistent storage, the browser provides a storage option called localStorage
. I used it to store the todos — and found two different ways to do this:
First Way - The Manual Way
I had four methods to take care of, and in each function, I had to manually update the list.
handleSubmit
: submission of form data
const handleSubmit = e => { e.preventDefault() const newList = [...list, { id: Math.random().toString(16).slice(2), task, completed: false }] setList(newList) // storing in localStorage after new task localStorage.setItem('todos', JSON.stringify(newList)) }
handleDelete
: deleting an individual item from the list
const handleDelete = (id) => { const newList = list.filter(item => item.id !== id) setList(newList) localStorage.setItem('todos', JSON.stringify(newList)) }
handleComplete
: toggle completion of individual items
const handleComplete = id => { const newList = list.map(item => item.id === id ? { ...item, completed: !item.completed } : item) setList(newList) localStorage.setItem('todos', JSON.stringify(newList)) }
handleClear
: clear the completed todos
const handleClear = () => { const newList = list.filter(item => item.completed !== true) setList(newList) localStorage.setItem('todos', JSON.stringify(newList)) }
I had to update localStorage manually in every function. It’s okay at this level of the project, but in larger projects, this can become quite messy.
Second Way - Using useEffect
Hook
I used two useEffect
s to get the desired output.
The first one loads the stored list when the page first loads,
and the second one updates localStorage whenever the list changes.
useEffect(() => { const newList = localStorage.getItem('todos') if (newList) setList(JSON.parse(newList)) }, []) useEffect(() => { localStorage.setItem('todos', JSON.stringify(list)) }, [list])
But here’s the catch: on the very first render, when the page loads, the list state resets to empty — and that empty list gets saved back to localStorage, wiping everything.
So I had to prevent this.
The way I did that was by using useRef
, since useRef
doesn’t affect re-renders.
const firstRender = useRef(true) useEffect(() => { const newList = localStorage.getItem('todos') if (newList) setList(JSON.parse(newList)) }, []) useEffect(() => { if (firstRender.current) { firstRender.current = false // change firstRender value to false return // if still first render, return } localStorage.setItem('todos', JSON.stringify(list)) }, [list])
So this method also has its issues, but it was far better than manually coding localStorage updates in every function.
For me, the useEffect
method was clearly the better choice over the first one.
Thanks for reading my first post! What would you have done differently — the manual way or useEffect? Let me know your thoughts 😊
Top comments (16)
You created the extra render cycle yourself, because of the first side effect that you use to set the initial state. You can get rid of that by providing a function to useState to retrieve the initial state from local storage:
joshwcomeau.com/react/persisting-r...
Some general advice: You should really be careful what you use side effects for, using them incorrectly can create very serious bugs and performance issues in your app.
Thanks for the suggestion, will certainly look in the side effects, so that I'll be more careful next time.
Hello dev
Hey
I am up 24hours man
why ??
Pls can I dm you
You can hire me as a mod I will handle things for you just $50 per week
Thanks for the offer, but I just started posting, If I need something like this, I will certainly contact you.
Ok but can I get your telegram or twitter username so it will be more easier sir
You might be able to find a hook on NPM to handle the syncing of storage for you, like @blocdigital/uselocalstorage, which may make your life a little easier.
Here's a quick example of how it would fit in with your example (though I've not tested it).
Thanks for the feedback and snippet, I will certainly look more in the hook and see how can i implement it to make the app better, and after trying maybe write a follow up post too.
The last time I used that hook it needed 3 render cycles to display the actual local storage value... I hope it has improved since then.
I don't think the hook in my specific example has had that issue. Or at least it's not an issue I've ever noticed but I did write it.
Oh btw, local storage actually produces events, so you can attach event listeners to it in a side effect (what they're actually meant for) and update state from there. Dont forget to remove the event listener in the cleanup function of the side effect though.
Here's a codesandbox, this will also work if you change local storage in other tabs:
codesandbox.io/p/devbox/late-sun-q...
I didn’t know about the events associated with localStorage. I’ll definitely explore that more and make sure to handle cleanup properly when adding event listeners in a side effect. Thanks a lot for the sandbox — really helpful!