常用模式
这一页用几个完整场景说明 Store 应该放在哪里、怎么拆,以及什么时候让一个 Store 读取另一个 Store。
用 Provider props 创建不同实例
文档编辑器通常需要根据 documentId 加载不同内容。把 documentId 作为 Provider prop:
interface DocumentProps {
documentId: string
}
function useDocumentStoreValue({ documentId }: DocumentProps) {
const document = useDocumentQuery(documentId)
return {
documentId,
content: document.data?.content ?? '',
loading: document.loading,
save: document.save,
}
}
const [useDocument, DocumentProvider] = createStore(useDocumentStoreValue)
使用时,每个 Provider 都会按自己的 documentId 创建 Store:
<DocumentProvider documentId="readme">
<Editor />
</DocumentProvider>
<DocumentProvider documentId="changelog">
<Preview />
</DocumentProvider>
两个实例的数据完全隔离。documentId 更新时,Store Hook 会像普通组件一样重新执行,并发布提交后的新结果。
只调用一次 SDK Hook
假设 useChatStream 会创建 SSE 连接并维护消息缓存,就只在一个 Store 中调用它:
function useStreamStoreValue() {
const stream = useChatStream()
return {
messages: stream.messages,
running: stream.running,
error: stream.error,
send: stream.send,
stop: stream.stop,
}
}
const [useStream, StreamProvider] = createStore(useStreamStoreValue)
消息列表只取消息:
const { messages } = useStream(s => ({ messages: s.messages }))
停止按钮只取运行状态和动作:
const { running, stop } = useStream(s => ({
running: s.running,
stop: s.stop,
}))
不要在消息列表、输入框和导航 Store 中分别调用 useChatStream,否则可能创建多条连接和多份缓存。
拆分一个聊天 Store
当一个 Store 同时保存消息、当前线程、导航弹窗和输入草稿时,任何更新都可能生成新的大快照。可以按职责拆开:
Stream → Thread
├──→ Navigation
└──→ Sender
Stream 持有唯一 SDK 连接。Thread 从 Stream 读取消息并生成当前线程视图:
function useThreadStoreValue() {
const { messages } = useStream(s => ({ messages: s.messages }))
const visibleMessages = useMemo(
() => messages.filter(message => !message.hidden),
[messages],
)
return { messages: visibleMessages }
}
const [useThread, ThreadProvider] = createStore(useThreadStoreValue)
Sender 保存输入草稿,并从 Stream 取得发送动作:
function useSenderStoreValue() {
const { send } = useStream(s => ({ send: s.send }))
const [draft, setDraft] = useState('')
const submit = () => {
if (!draft.trim())
return
send(draft)
setDraft('')
}
return { draft, setDraft, submit }
}
const [useSender, SenderProvider] = createStore(useSenderStoreValue)
最后按依赖顺序挂载:
<StreamProvider>
<ThreadProvider>
<NavigationProvider>
<SenderProvider>
<App />
</SenderProvider>
</NavigationProvider>
</ThreadProvider>
</StreamProvider>
输入草稿更新时,只会发布 Sender Store 的快照。只订阅 Thread 或 Navigation 的组件不会因此更新。
公共动作直接写普通函数
Store 暴露给按钮、表单或其他组件调用的动作,直接写成普通函数:
const submit = () => {
send(draft)
}
return { draft, submit }
如果项目启用了 React Compiler,让 Compiler 处理它能够安全稳定的引用。没有启用 Compiler 时,按照普通 React 规则决定是否需要 useCallback。
useEffectEvent 只适合 Effect 内部调用的事件,不要用它包装 submit 这类公共动作。
让 Provider 靠近使用位置
并不是所有 Store 都要放在应用根部:
- 主题、账户等全应用状态可以放在根部
- 文档 Store 放在编辑器路由外层
- 表单 Store 放在弹窗或页面外层
- 可复用组件的 Store 放在组件自己的入口
Provider 越靠近真正的使用位置,Store 的生命周期和作用域就越容易理解。