Next.js
Next.js 集成的关键是:每请求创建状态、只传可序列化数据、客户端重建 store。
- App Router 下的 Server + Client 组合页面。
- 需要服务端预取数据并首屏渲染。
- 需要避免跨请求共享状态导致串数据。
- 每请求 Store:不要在模块顶层创建全局 store。
- Hydrate 边界:只传 plain object,不传 store 实例。
- 传值流向:
server fetch -> initialState -> client createStore。
每请求 Store
Section titled “每请求 Store”不要在模块顶层放全局 store:
// bad: 整个 Node 进程共享export const store = io({ count: 0 });改成工厂函数:
import { io } from '@iostore/store';
export type AppState = { count: number; user: { id: string; name: string } | null;};
export function createStore(initial: AppState) { return io(initial);}Hydrate 边界
Section titled “Hydrate 边界”// app/dashboard/page.tsx (Server Component)import { createStore } from '@/lib/store';import { DashboardClient } from './dashboard-client';
export default async function Page() { const user = await fetch('https://example.com/api/me').then((r) => r.json()); const serverStore = createStore({ count: 0, user }); return <DashboardClient initialState={serverStore.snapshot()} />;}规则:边界只传 initialState 这种可序列化对象。
Client 侧重建 Store
Section titled “Client 侧重建 Store”'use client';
import { useRef } from 'react';import { useIO } from '@iostore/react';import { createStore, type AppState } from '@/lib/store';
export function DashboardClient({ initialState }: { initialState: AppState }) { const storeRef = useRef<ReturnType<typeof createStore> | null>(null); if (!storeRef.current) { storeRef.current = createStore(initialState); }
const count = useIO(storeRef.current.count); return <button onClick={() => storeRef.current?.count.set((v) => v + 1)}>{count}</button>;}API 速查
Section titled “API 速查”| 阶段 | API | 说明 |
|---|---|---|
| 创建初始状态 | io(initialState) | 每请求创建,避免跨请求污染。 |
| 生成边界数据 | scope.snapshot() | 产出可序列化初始数据。 |
| 客户端重建 | createStore(initialState) | Hydration 后恢复运行时能力。 |
| 组件订阅 | useIO(unitOrDerived) | 在 Client Component 中读取。 |
| 客户端写入 | `unit.set(next | updater)` |
错误示例与推荐写法
Section titled “错误示例与推荐写法”// 错误:模块级单例(SSR 会跨请求共享)export const store = io({ count: 0 });
// 推荐:工厂函数按请求创建export const createStore = (initial: AppState) => io(initial);// 错误:传 store 实例到 Client Component// <Client store={serverStore} />
// 推荐:只传 serializable initialState// <Client initialState={serverStore.snapshot()} />- 把
io(...)实例作为 props 传到 Client Component。 - 模块级单例 store 导致多用户状态串扰。
- 首屏数据与客户端初始化数据不一致导致 hydration 警告。
- 用类型约束
initialState,避免边界数据漂移。 - 复杂页面按路由或业务模块拆分多个 store。
- 首屏后再接入客户端行为扩展(如
persist)。