为什么在 useEffect 中的初始化对于加载 localStorage 不起作用?

huangapple go评论70阅读模式
英文:

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(() =&gt; {
//   if (localStorage.getItem(LOCAL_STORAGE_KEY)) {
//     const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
//     return storedTodos;
//   }
//   else
//     return [];
// })
const todoNameRef = useRef()
// This doesn&#39;t work as expected
//
useEffect(() =&gt; {
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if (storedTodos) {
console.log(&quot;storedTodos size: &quot;, storedTodos.length)
setTodos(storedTodos)
}
}, [])
useEffect(() =&gt; {
console.log(&quot;todos size: &quot;, todos.length)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
function handleAddTodo(e) {
const name = todoNameRef.current.value
if (name === &#39;&#39;) return
setTodos(prevTodos =&gt; {
return [...prevTodos, { id: uuidv4(), name: name, complete: false }]
})
todoNameRef.current.value = null
}
return (
&lt;&gt;
&lt;TodoList todos={todos} /&gt;
&lt;input ref={todoNameRef} type=&quot;text&quot; /&gt;
&lt;button onClick={handleAddTodo}&gt;Add Todo&lt;/button&gt;
&lt;button&gt;Clear Completed Todos&lt;/button&gt;
&lt;div&gt;0 left to do&lt;/div&gt;
&lt;/&gt;
)
}
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) =&gt; {
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 &gt; 0).

  useEffect(() =&gt; {
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
// added check
if (storedTodos &amp;&amp; storedTodos.length &gt; 0) {
console.log(&quot;storedTodos size: &quot;, 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在单个渲染中调用两次(在开发期间),因此操作顺序如下:

  1. 读取localStorage,使用存储的值调用setTodos
  2. []写入localStorage
  3. 从localStorage中读取[],使用[]调用setTodos
  4. 再次将[]写入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 useEffects.

  const [todos, _setTodos] = useState(() =&gt; {
    const storedTodosString = localStorage.getItem(LOCAL_STORAGE_KEY);
    if (!storedTodosString) return [];
    return JSON.parse(storedTodosString);
  });

  function setTodos(todos) {
    if (typeof todos === &#39;function&#39;) {
      _setTodos((prevTodos) =&gt; {
        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:

  1. Read localStorage, call setTodos with stored value
  2. Write [] to localStorage
  3. Read [] from localStorage, call setTodos with []
  4. Write [] to localStorage.

huangapple
  • 本文由 发表于 2023年7月17日 09:55:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/76701099.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定