Next.js中如何正确实现避免水合错误的useLocalStorage Hook
Next.js 场景下 useLocalStorage 的正确实现
现有两版实现的核心缺陷
- 第一版(在useState初始化逻辑中直接读取localStorage):
虽然加了typeof window === "undefined"的判断来规避服务端window不存在的问题,但服务端渲染输出的内容永远基于初始值生成,客户端hydration阶段如果本地存储的值和初始值不一致,首次渲染生成的DOM和服务端返回的HTML结构不匹配,就会直接触发hydration mismatch报错,也就是你碰到的渲染错误。 - 第二版(拆分两个useEffect分别处理读写):
- 首次渲染永远会先返回默认值,等useEffect执行完成后才会替换为本地存储的真实值,很容易出现页面内容闪烁的问题
- 没有增加挂载完成的控制标记,组件首次挂载阶段,写入localStorage的effect可能在读取旧值的effect之前触发,直接把默认值写入存储覆盖之前的持久化状态,这就是偶发状态丢失的根本原因
- 自定义的更新方法没有兼容函数式更新写法,和原生useState的API不对齐,碰到依赖前序状态更新的场景会出现逻辑错误
- 没有做异常兜底,遇到浏览器隐私模式禁用localStorage、JSON序列化失败等场景会直接抛出异常导致页面崩溃
- 适配Next.js App Router时没有加客户端组件标记,在服务端组件中调用会直接报错。
可直接使用的适配版实现
这个实现完全对齐原生useState的API,同时解决了上述所有问题:
'use client' import { useState, useEffect, useCallback, Dispatch, SetStateAction } from 'react' function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>] { // 初始化阶段统一使用初始值,保证服务端、客户端首次渲染输出完全一致,避免hydration报错 const [storedValue, setStoredValue] = useState<T>(initialValue) // 标记hydration完成状态,避免初始阶段误写入覆盖旧值 const [isMounted, setIsMounted] = useState(false) // 组件挂载完成后再读取本地存储的真实值 useEffect(() => { try { const rawValue = window.localStorage.getItem(key) setStoredValue(rawValue ? JSON.parse(rawValue) : initialValue) } catch (err) { console.error(`读取localStorage[${key}]失败:`, err) setStoredValue(initialValue) } finally { setIsMounted(true) } }, [key, initialValue]) // 自定义状态更新方法,对齐useState原生用法 const setValue: Dispatch<SetStateAction<T>> = useCallback((newValue) => { try { // 支持函数式更新 const valueToStore = newValue instanceof Function ? newValue(storedValue) : newValue setStoredValue(valueToStore) // 仅在挂载完成后才写入持久化存储,避免初始阶段覆盖旧值 if (isMounted && typeof window !== 'undefined') { window.localStorage.setItem(key, JSON.stringify(valueToStore)) // 触发storage事件,同步同页面、跨标签页的同key状态 window.dispatchEvent(new StorageEvent('storage', { key })) } } catch (err) { console.error(`写入localStorage[${key}]失败:`, err) } }, [key, storedValue, isMounted]) // 监听storage变更,自动同步跨标签页、同页面其他组件的状态更新 useEffect(() => { if (!isMounted) return const handleStorageChange = (e: StorageEvent) => { if (e.key !== key) return try { const syncedValue = e.newValue ? JSON.parse(e.newValue) : initialValue setStoredValue(syncedValue) } catch (err) { console.error(`同步localStorage[${key}]变更失败:`, err) } } window.addEventListener('storage', handleStorageChange) return () => window.removeEventListener('storage', handleStorageChange) }, [key, initialValue, isMounted]) return [storedValue, setValue] } export default useLocalStorage
使用注意事项
- 该Hook仅可在客户端组件中使用:Next.js Pages Router下可直接调用,App Router下需要在使用该Hook的组件顶部添加
'use client'标记 - 如果对首屏内容一致性要求高,可以结合返回的挂载状态做加载态判断,比如未挂载完成时展示骨架屏,等真实存储值读取完成后再渲染实际内容,避免内容闪烁
- 不要存入无法被JSON序列化的值(比如函数、Symbol、存在循环引用的对象),否则会导致存取逻辑异常。
内容的提问来源于stack exchange,提问作者jpceia
相关产品推荐
相关产品推荐

