ReactJS中localStorage设置异常:首次点击存入空数组
问题解决:useLocalStorage首次存入空数组的问题
问题场景
我是React和TypeScript新手,尝试通过自定义useLocalStorage Hook将值存入localStorage。首次点击“Set Item”按钮时,localStorage会存入空数组,后续操作则正常。已尝试用useEffect初始化空数组,但首次添加数据仍无效,附上相关代码寻求解决方法。
相关代码
hooks/useLocalStorage.ts
function useLocalStorage(key: string) { /* Set Item to Localstorage */ const set = (value: unknown) => { try { localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.log((error as Error)?.message); } }; /* Get Item from Localstorage */ const get = () => { try { return localStorage.getItem(key); } catch (error) { console.log(error); } }; /* Remove Item */ const clean = () => { try { localStorage.removeItem(key); } catch (error) { console.log((error as Error)?.message); } }; return { set, get, clean }; } export { useLocalStorage };
App.tsx
import { Button, ButtonGroup, TextField } from "@mui/material"; import { useState } from "react"; import { useLocalStorage } from "./hooks/useLocalStorage"; function App() { type TProp = { title: string; }; const [value, setValue] = useState(""); const { set, get, clean } = useLocalStorage("todos"); const [todos, setTodos] = useState<TProp[]>([]); /* Set Item */ const setItem = () => { setTodos([...todos, { title: value }]); set(todos); }; return ( <> <TextField label="Enter Text" variant="outlined" fullWidth margin="normal" value={value} onChange={(e) => setValue(e.target.value)} /> <ButtonGroup variant="contained" fullWidth> <Button onClick={setItem}>Set Item</Button> <Button color="success" onClick={() => console.log(get())}> Get Item </Button> </ButtonGroup> <Button variant="contained" color="error" fullWidth sx={{ mt: 2 }} onClick={clean} > Remove Item </Button> </> ); } export default App;
main.tsx
import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode>, )
尝试过的无效方案
曾使用useEffect初始化空数组,但首次添加数据仍无效:
/** ... **/ useEffect(() => { if(get()?.length === 0) { /* todos is empty at this moment */ set(todos) } },[]) /** ... **/
问题原因
核心问题在于setTodos是异步更新状态的函数:在setItem中,调用setTodos后立刻用当前的todos变量(初始空数组)调用set,此时新的todos状态还未更新完成,所以首次存入的是旧的空数组。后续点击时,todos已经是之前更新后的数组,所以存入正常。
解决方案
方案1:直接使用新创建的数组(快速修复)
修改App.tsx中的setItem函数,先创建新数组,同时更新状态和localStorage,避免依赖异步更新的状态:
const setItem = () => { const newTodos = [...todos, { title: value }]; setTodos(newTodos); set(newTodos); setValue(""); // 可选:清空输入框提升体验 };
方案2:优化useLocalStorage Hook(推荐)
让Hook内部维护状态,自动同步localStorage,更符合React Hooks的设计逻辑,也避免手动同步的错误:
修改后的hooks/useLocalStorage.ts
import { useState, useEffect } from "react"; function useLocalStorage<T>(key: string, initialValue: T) { // 从localStorage读取初始值,没有则用传入的初始值 const [storedValue, setStoredValue] = useState<T>(() => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.log((error as Error)?.message); return initialValue; } }); // 支持函数式更新,同时同步到localStorage const setValue = (value: T | ((val: T) => T)) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.log((error as Error)?.message); } }; // 监听全局storage变化,处理其他页面修改localStorage的情况(可选) useEffect(() => { const handleStorageChange = (e: StorageEvent) => { if (e.key === key) { setStoredValue(e.newValue ? JSON.parse(e.newValue) : initialValue); } }; window.addEventListener("storage", handleStorageChange); return () => window.removeEventListener("storage", handleStorageChange); }, [key, initialValue]); // 清除localStorage并重置状态 const clean = () => { try { localStorage.removeItem(key); setStoredValue(initialValue); } catch (error) { console.log((error as Error)?.message); } }; return { value: storedValue, setValue, clean }; } export { useLocalStorage };
修改后的App.tsx
import { Button, ButtonGroup, TextField } from "@mui/material"; import { useState } from "react"; import { useLocalStorage } from "./hooks/useLocalStorage"; function App() { type TProp = { title: string; }; const [value, setValue] = useState(""); // 使用优化后的Hook,指定初始值为空数组 const { value: todos, setValue: setTodos, clean } = useLocalStorage<TProp[]>("todos", []); const setItem = () => { // 使用函数式更新,确保拿到最新的状态 setTodos(prevTodos => [...prevTodos, { title: value }]); setValue(""); // 清空输入框 }; return ( <> <TextField label="Enter Text" variant="outlined" fullWidth margin="normal" value={value} onChange={(e) => setValue(e.target.value)} /> <ButtonGroup variant="contained" fullWidth> <Button onClick={setItem}>Set Item</Button> <Button color="success" onClick={() => console.log(todos)}> Get Item </Button> </ButtonGroup> <Button variant="contained" color="error" fullWidth sx={{ mt: 2 }} onClick={clean} > Remove Item </Button> </> ); } export default App;
这种方案下,状态和localStorage会自动同步,无需手动调用set,彻底避免了异步状态更新导致的问题,同时还支持跨页面的localStorage同步。
内容的提问来源于stack exchange,提问作者RKS
相关产品推荐
相关产品推荐

