You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 11:06:19