🧩 前言
在使用 Zustand 进行状态管理时,我们很容易把注意力集中在组件本身,却忽略了状态模块背后的执行环境。当我在 Next.js App Router 中封装 useSortStore 自定义 hook 时,遇到一个看似莫名其妙的错误:
Error: Switched to client rendering because the server rendering errored:
Cannot read properties of null (reading 'useSyncExternalStore')
这段报错是我意识到“不是组件也要写 use client”的起点。
😵💫 问题1
// store/useSortStore.ts
import { useStore } from 'zustand'
import { createStore } from 'zustand/vanilla'
const sortStore = createStore(() => ({
sortTag: 'latest',
setSort: (value) => set({ sortTag: value }),
}))
const useSortStore = (selector) => useStore(sortStore, selector)
export default useSortStore
// components/Sort.tsx
"use client"
import useSortStore from '@/store/useSortStore'
export function Sort() {
const sortTag = useSortStore(s => s.sortTag)
// ...
}
你以为没问题?但其实还是会在 SSR 阶段崩溃。
🧠 原因解析:模块顶层的“副作用”也会在 SSR 中运行
虽然组件是 "use client" 的,但 useSortStore.ts 模块没有 use client,所以:
即使你从未在服务端主动调用这些代码,只要组件中 import 了它,就会连带执行 SSR 预加载阶段的副作用。
✅ 结论:只要模块中用了 hook,不管是不是组件,都必须标记 "use client"
😵💫问题升级:标记"use client", 依然尝试在server运行?
彻底解决:zustand中createStore需要懒加载。在客户端组件初始化的时候,再运行!
let _store: StoreApi<SortStore> | null = null
function initSortStore() {
return createStore<SortStore>((set) => ({
sortTag: 'latest',
setSort: (tag) => set({ sortTag: tag }),
}))
}
function getStore(): StoreApi<SortStore> {
if (!_store) {
_store = initSortStore()
}
return _store
}
function useSortStore<T>(selector: (state: SortStore) => T) {
const store = getStore()
return useStore(store, selector)
}
export default useSortStore
✅ 结论:zustand的全局store创建,需要lazy initialization
😵💫 可能仍然有问题?
✅ 稳定写法建议
-
✅ 必须放在文件最顶部(import 之前);
-
✅ 必须使用 双引号:"use client";
😡 还是有问题,真相是?
推翻了以上所有结论,答案是使用create-next-app 的默认npm run dev 会使用Turbopack(热更新还有bug!!!),而非 webpack!!!
所以问题出在Turbopack!!!!!!!!!
解决:
1. 安装 npm install --save-dev cross-env
2. 修改package.json
"scripts": {
"dev": "cross-env TURBOPACK=0 next dev"
}
不要用Turbopack!!!!!!!!!!!
来源:https://www.cnblogs.com/sabertobih/p/18934485 |