英文:
Why the initialization from useEffect doesn't work for loading localStorage?
问题
以下是您提供的代码的翻译部分:
我遇到了一个问题,无法通过 `useEffect` 从 `localStorage` 初始化 `todos`。在将两个待办事项添加到列表中,然后单击浏览器刷新按钮后,所有待办事项都无法重新加载。跟踪结果如下:
storedTodos 大小:2
App.js:41 todos 大小:0
App.js:34 storedTodos 大小:0
App.js:41 todos 大小:0
App.js:41 todos 大小:0
对我来说,似乎 `setTodos(storedTodos)` 不按预期工作。解决方法是通过 `useState` 加载初始化,如下所示。
**问题**:请问有人可以告诉我为什么这个初始化在 `useEffect` 中不起作用吗?
谢谢
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()
// 这个不按预期工作
//
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if (storedTodos) {
console.log("storedTodos 大小:", storedTodos.length)
setTodos(storedTodos)
}
}, [])
useEffect(() => {
console.log("todos 大小:", 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}>添加待办事项</button>
<button>清除已完成的待办事项</button>
<div>剩余待办事项数:0</div>
</>
)
}
export default App;
希望这对您有所帮助。如果您需要任何进一步的帮助,请随时告诉我。
英文:
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;
答案1
得分: 1
我认为问题在于您的useEffect
钩子以todos
作为依赖项,每当todos状态发生变化时都会运行,包括初始渲染时todos为null
。这会导致localStorage被覆盖为一个空数组,从而阻止其他useEffect
钩子加载存储的todos。
为了解决这个问题,您可以将localStorage逻辑移至setTodos函数内部,并移除以todos
作为依赖的useEffect
钩子。
例如:
setTodos((prevTodos) => {
const todos = [...prevTodos, { id: uuidv4(), name: name, complete: false }];
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos));
return todos;
});
英文:
I think the problem is that your useEffect
hook with todos
as dependency runs every time the todos state changes, which includes the initial render when todos is null
. This causes the localStorage to be overwritten with an empty array, which then prevents the other useEffect
hook from loading the stored todos.
To fix this, you can move the localStorage logic inside the setTodos function and remove useEffect
with todos
as dependency.
For example:
setTodos((prevTodos) => {
const todos = [...prevTodos, { id: uuidv4(), name: name, complete: false }];
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos));
return todos;
});
答案2
得分: 1
尝试添加一个检查,确保状态只有在来自localStorage的值已经解析后才设置。将if(storedTodos)
替换为if(storedTodos.length > 0)
。
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
// 添加了检查
if (storedTodos && storedTodos.length > 0) {
console.log("storedTodos size: ", storedTodos.length)
setTodos(storedTodos)
}
}, [])
英文:
Try to add a check to ensure the state is only set if the value from localStorage has resolved. Replace if(storedTodos)
with if(storedTodos.length > 0)
.
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
// added check
if (storedTodos && storedTodos.length > 0) {
console.log("storedTodos size: ", storedTodos.length)
setTodos(storedTodos)
}
}, [])
答案3
得分: 1
我不会在这种情况下使用useEffect
。使用您的注释代码来初始化状态变量。然后创建一个setTodos
的包装函数来缓存该值。删除两个useEffect
。
const [todos, _setTodos] = useState(() => {
const storedTodosString = localStorage.getItem(LOCAL_STORAGE_KEY);
if (!storedTodosString) return [];
return JSON.parse(storedTodosString);
});
function setTodos(todos) {
if (typeof todos === 'function') {
_setTodos((prevTodos) => {
const newTodos = todos(prevTodos);
if (!newTodos) return prevTodos;
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newTodos));
return newTodos;
});
} else {
if (!todos) return;
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos));
_setTodos(todos);
}
}
工作示例:https://stackblitz.com/edit/stackblitz-starters-awp3f5?file=src%2FApp.tsx
您的解决方案不起作用,因为状态变量在渲染期间是不可变的。在第一次渲染时,无论您调用setTodos
多少次,todos
的值都是[]
。这意味着您在初始化时将[]
写入了localStorage。Strict Mode导致useEffect
在单个渲染中调用两次(在开发期间),因此操作顺序如下:
- 读取localStorage,使用存储的值调用
setTodos
- 将
[]
写入localStorage - 从localStorage中读取
[]
,使用[]
调用setTodos
- 再次将
[]
写入localStorage。
英文:
I would not use useEffect
at all in this scenario. Use your commented code to initialize the state variable. Then make a wrapper for setTodos
to cache the value. Delete both useEffect
s.
const [todos, _setTodos] = useState(() => {
const storedTodosString = localStorage.getItem(LOCAL_STORAGE_KEY);
if (!storedTodosString) return [];
return JSON.parse(storedTodosString);
});
function setTodos(todos) {
if (typeof todos === 'function') {
_setTodos((prevTodos) => {
const newTodos = todos(prevTodos);
if (!newTodos) return prevTodos;
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newTodos));
return newTodos;
});
} else {
if (!todos) return;
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos));
_setTodos(todos);
}
}
Working example: https://stackblitz.com/edit/stackblitz-starters-awp3f5?file=src%2FApp.tsx
Your solution doesn't work because state variables are immutable during a render. On your first render, the value of todos
is []
no matter how many times you call setTodos
. This means you are writing []
to localStorage on init. Strict Mode causes useEffects
to be called twice in a single render (during development) so order of operations goes:
- Read localStorage, call
setTodos
with stored value - Write
[]
to localStorage - Read
[]
from localStorage, callsetTodos
with[]
- Write
[]
to localStorage.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论