Reference:
https://youtu.be/hQAHSlTtcmY?t=1342
I had an issue where I cannot initialize the todos
from useEffect
through localStorage
. After adding two ToDo into the list and then click browser refresh button, all ToDo will not be able to load back. The trace looks as below:
storedTodos size: 2
App.js:41 todos size: 0
App.js:34 storedTodos size: 0
App.js:41 todos size: 0
App.js:41 todos size: 0
It seems to me the setTodos(storedTodos)
doesn’t work as expected. The solution is to load the initialization through useState
shown below.
Question> Can someone please advice me that why this initialization doesn’t work within useEffect
?
Thank you
function App() {
const [todos, setTodos] = useState([])
// const [todos, setTodos] = useState(() => {
// if (localStorage.getItem(LOCAL_STORAGE_KEY)) {
// const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
// return storedTodos;
// }
// else
// return [];
// })
const todoNameRef = useRef()
// This doesn't work as expected
//
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if (storedTodos) {
console.log("storedTodos size: ", storedTodos.length)
setTodos(storedTodos)
}
}, [])
useEffect(() => {
console.log("todos size: ", todos.length)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
function handleAddTodo(e) {
const name = todoNameRef.current.value
if (name === '') return
setTodos(prevTodos => {
return [...prevTodos, { id: uuidv4(), name: name, complete: false }]
})
todoNameRef.current.value = null
}
return (
<>
<TodoList todos={todos} />
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Completed Todos</button>
<div>0 left to do</div>
</>
)
}
export default App;
3
Answers
I think the problem is that your
useEffect
hook withtodos
as dependency runs every time the todos state changes, which includes the initial render when todos isnull
. This causes the localStorage to be overwritten with an empty array, which then prevents the otheruseEffect
hook from loading the stored todos.To fix this, you can move the localStorage logic inside the setTodos function and remove
useEffect
withtodos
as dependency.For example:
Try to add a check to ensure the state is only set if the value from localStorage has resolved. Replace
if(storedTodos)
withif(storedTodos.length > 0)
.You just want to avoid caching the todos on the first render.
Note that state variables are immutable during a render cycle.
setState
updates the value for the next render. That means that for the entire first render,todos
is[]
, no matter what you do. So allowing this function to run on the first render will overwrite your localStorage to[]
.The solution you already have with the commented out code is better imo, since it immediately instantiates the state variable rather than waiting for a whole render cycle.
To give you a tip from what I saw in the comments. If you want the
TodoList
component to be able to update the state (ie. to check an item), you can pass in thesetState
function as well.An option that plays nicely with Strict Mode is to just create a wrapper for
setTodos
Now just call the new
setTodos
and remove theuseEffect
with thetodos
dependency.