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

在Next.js 13+ App Router中使用useSyncExternalStore是否存在问题?

在Next.js 13+ App目录中使用useSyncExternalStore实现全局存储的问题与建议

是的,你用useSyncExternalStore实现的这个全局存储会遇到和Redux全局存储完全一样的核心问题,根源在于Next.js App Router的请求处理机制。

核心问题:服务器端状态污染与Hydration不匹配

Next.js App Router的服务器端是无状态并发处理——多个用户的请求会共享同一个Node.js进程里的模块级变量。你的代码里store是模块级的全局变量,这会导致:

  • 跨用户数据污染:用户A的操作会修改全局store,直接影响用户B的请求结果,比如用户A把count改成5,用户B打开页面看到的初始count也是5而不是0
  • Hydration错误:服务器渲染时用的是全局初始store值,但客户端可能已经有修改后的状态,导致客户端渲染的内容和服务器输出不匹配,触发React hydration校验错误

你的代码的具体风险

  • 模块级的store和listeners在服务器端是所有请求共用的,只要有一个请求在服务器端调用setStore,后续所有请求都会拿到修改后的状态
  • 即使只在客户端组件里调用setStore,服务器端SSR时getStore()返回的始终是初始值,客户端hydrate时如果已经有修改后的状态,就会出现内容不一致

实用建议

  1. 彻底抛弃模块级全局存储:不要在模块层面定义共享状态,每个用户/请求必须有独立的状态实例
  2. 用React Context包裹状态:把store实例放在Context里,确保每个用户的客户端有独立的上下文(示例代码如下)
  3. 服务器端状态直接从数据源获取:服务器组件不要依赖全局存储,尽量通过async/await直接调用API或数据库获取数据
  4. 如果需要跨组件共享客户端状态:结合useSyncExternalStore和Context,让每个客户端有自己的store实例,而不是全局共享

修改后的示例代码(安全版本)

// app/store-context.js
'use client';

import { createContext, useContext, useSyncExternalStore } from 'react';

const StoreContext = createContext(null);

export function StoreProvider({ children, initialStore }) {
  const [useStore, setStore] = createStore(initialStore);
  
  return (
    <StoreContext.Provider value={{ useStore, setStore }}>
      {children}
    </StoreContext.Provider>
  );
}

function createStore(initialStore) {
  let store = initialStore;
  const listeners = new Set();

  function setStore(newStore) {
    store = newStore;
    listeners.forEach(listener => listener(store));
  }

  function subscribe(listener) {
    listeners.add(listener);
    return () => listeners.delete(listener);
  }

  function getStore() {
    return store;
  }

  function useStore() {
    return useSyncExternalStore(subscribe, getStore);
  }

  return [useStore, setStore];
}

export function useStoreContext() {
  const context = useContext(StoreContext);
  if (!context) {
    throw new Error('useStoreContext must be used within a StoreProvider');
  }
  return context;
}

在根布局中使用Provider:

// app/layout.js
import { StoreProvider } from './store-context';

export default function RootLayout({ children }) {
  return (
    <html lang="zh-CN">
      <body>
        <StoreProvider initialStore={{ count: 0 }}>
          {children}
        </StoreProvider>
      </body>
    </html>
  );
}

然后在客户端组件中使用:

// app/page.js
'use client';

import { useStoreContext } from './store-context';

export default function Home() {
  const { useStore, setStore } = useStoreContext();
  const store = useStore();

  return (
    <div>
      <p>Count: {store.count}</p>
      <button onClick={() => setStore({ count: store.count + 1 })}>
        加1
      </button>
    </div>
  );
}

这样每个用户的客户端都会拥有独立的store实例,服务器端也不会出现状态共享的问题。

内容的提问来源于stack exchange,提问作者user3331344

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 09:18:32