荒唐镜 發表於 2025-6-18 11:38:00

Next.js + Zustand:Switched to client rendering because the server rendering errored 原因探索

<h1>🧩 前言</h1>
<p>在使用 Zustand 进行状态管理时,我们很容易把注意力集中在组件本身,却忽略了状态模块背后的执行环境。当我在 Next.js App Router 中封装 <code data-start="288" data-end="302">useSortStore</code> 自定义 hook 时,遇到一个看似莫名其妙的错误:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">Error: Switched to client rendering because the server rendering errored:
Cannot read properties of </span><span style="color: rgba(0, 0, 255, 1)">null</span> (reading 'useSyncExternalStore')</pre>
</div>
<p data-start="477" data-end="514"><strong data-start="477" data-end="514">这段报错是我意识到“不是组件也要写 use client”的起点。</strong></p>
<h1 data-start="477" data-end="514"><strong data-start="477" data-end="514">😵‍💫 问题1</strong></h1>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> store/useSortStore.ts</span>
import { useStore } from 'zustand'<span style="color: rgba(0, 0, 0, 1)">
import { createStore } from </span>'zustand/vanilla'<span style="color: rgba(0, 0, 0, 1)">

const sortStore </span>= createStore(() =&gt;<span style="color: rgba(0, 0, 0, 1)"> ({
sortTag: </span>'latest'<span style="color: rgba(0, 0, 0, 1)">,
setSort: (value) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> set({ sortTag: value }),
}))

const useSortStore </span>= (selector) =&gt;<span style="color: rgba(0, 0, 0, 1)"> useStore(sortStore, selector)
export </span><span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)"> useSortStore

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> components/Sort.tsx</span>
"use client"<span style="color: rgba(0, 0, 0, 1)">
import useSortStore from </span>'@/store/useSortStore'<span style="color: rgba(0, 0, 0, 1)">

export </span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> Sort() {
const sortTag </span>= useSortStore(s =&gt;<span style="color: rgba(0, 0, 0, 1)"> s.sortTag)
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ...</span>
}</pre>
</div>
<p>你以为没问题?但其实<strong data-start="1084" data-end="1101">还是会在 SSR 阶段崩溃</strong>。</p>
<p data-start="1109" data-end="1141"><strong>🧠 原因解析:模块顶层的“副作用”也会在 SSR 中运行</strong></p>
<p data-start="1143" data-end="1211">虽然组件是 <code data-start="1149" data-end="1163">"use client"</code> 的,但 <code data-start="1168" data-end="1185">useSortStore.ts</code> 模块没有 <code data-start="1193" data-end="1205">use client</code>,所以:</p>
<ul data-start="1213" data-end="1346">
<li data-start="1213" data-end="1241">
<p data-start="1215" data-end="1241"><strong data-start="1215" data-end="1241">它会被当作 Server Module 加载</strong></p>
</li>
<li data-start="1242" data-end="1298">
<p data-start="1244" data-end="1298">导致模块顶层执行 <code data-start="1253" data-end="1268">createStore()</code> 并导入 <code data-start="1273" data-end="1285">useStore()</code> 等 React Hook</p>
</li>
<li data-start="1299" data-end="1346">
<p data-start="1301" data-end="1346">这些行为在 SSR 中触发 <code data-start="1315" data-end="1343">useSyncExternalStore(null)</code> 报错</p>
</li>
</ul>
<p data-start="1348" data-end="1409">即使你从未在服务端主动调用这些代码,<strong data-start="1366" data-end="1409">只要组件中 <code data-start="1374" data-end="1382">import</code> 了它,就会连带执行 SSR 预加载阶段的副作用。</strong></p>
<h2 data-start="1348" data-end="1409"><strong data-start="1366" data-end="1409">✅ 结论:只要模块中用了 hook,不管是不是组件,都必须标记 <code data-start="1451" data-end="1465">"use client"</code></strong></h2>
<hr>
<p><strong data-start="1366" data-end="1409">&nbsp;</strong></p>
<h1 data-start="477" data-end="514"><strong data-start="477" data-end="514">😵‍💫</strong>问题升级:标记"use client", 依然尝试在server运行?</h1>
<p data-start="477" data-end="514"><strong data-start="477" data-end="514">彻底解决:zustand中createStore需要懒加载。在客户端组件初始化的时候,再运行!</strong></p>
<div class="cnblogs_code">
<pre>let _store: StoreApi&lt;SortStore&gt; | <span style="color: rgba(0, 0, 255, 1)">null</span> = <span style="color: rgba(0, 0, 255, 1)">null</span>

<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> initSortStore() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> createStore&lt;SortStore&gt;((set) =&gt;<span style="color: rgba(0, 0, 0, 1)"> ({
      sortTag: </span>'latest'<span style="color: rgba(0, 0, 0, 1)">,
      setSort: (tag) </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> set({ sortTag: tag }),
    }))
}

</span><span style="color: rgba(0, 0, 255, 1)">function</span> getStore(): StoreApi&lt;SortStore&gt;<span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">_store) {
      _store </span>=<span style="color: rgba(0, 0, 0, 1)"> initSortStore()
    }
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> _store
}

</span><span style="color: rgba(0, 0, 255, 1)">function</span> useSortStore&lt;T&gt;(selector: (state: SortStore) =&gt;<span style="color: rgba(0, 0, 0, 1)"> T) {
    const store </span>=<span style="color: rgba(0, 0, 0, 1)"> getStore()
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> useStore(store, selector)
}

export </span><span style="color: rgba(0, 0, 255, 1)">default</span> useSortStore</pre>
</div>
<p>&nbsp;</p>
<h2 data-start="1348" data-end="1409"><strong data-start="1366" data-end="1409">✅ 结论:zustand的全局store创建,需要lazy initialization</strong></h2>
<hr>
<p>&nbsp;</p>
<h1 data-start="477" data-end="514"><strong data-start="477" data-end="514">😵‍💫&nbsp;</strong>可能仍然有问题?</h1>
<h2 data-start="816" data-end="827">✅ 稳定写法建议</h2>
<ul data-start="829" data-end="950">
<li data-start="829" data-end="854">
<p data-start="831" data-end="854">✅ 必须放在文件最顶部(import 之前);</p>
</li>
<li data-start="855" data-end="887">
<p data-start="857" data-end="887">✅ 必须使用 <strong data-start="864" data-end="871">双引号</strong>:<code data-start="872" data-end="886">"use client"</code>;</p>
</li>
</ul>
<h1>😡 还是有问题,真相是?</h1>
<p>推翻了以上所有结论,答案是使用create-next-app 的默认npm run dev 会使用Turbopack(热更新还有bug!!!),而非 webpack!!!</p>
<p>所以问题出在Turbopack!!!!!!!!!</p>
<p>解决:</p>
<p>1. 安装 npm install --save-dev cross-env</p>
<p>2. 修改package.json</p>
<div class="cnblogs_code">
<pre>"scripts"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"dev": "cross-env TURBOPACK=0 next dev"<span style="color: rgba(0, 0, 0, 1)">
}</span></pre>
</div>
<p>不要用Turbopack!!!!!!!!!!!</p><br><br>
来源:https://www.cnblogs.com/sabertobih/p/18934485
頁: [1]
查看完整版本: Next.js + Zustand:Switched to client rendering because the server rendering errored 原因探索