Supermemory状态管理策略:Zustand与TanStack Query的协同使用
引言:状态管理的双重挑战
你是否还在为前端应用中的状态管理而头疼?当用户数据、UI状态、服务器缓存交织在一起时,如何保证应用性能与开发效率的平衡?Supermemory作为一款构建第二大脑的知识管理工具,面临着既要处理复杂用户交互状态,又要高效同步服务器数据的双重挑战。本文将深入剖析Supermemory如何创新性地结合Zustand与TanStack Query,构建出既轻量又强大的状态管理架构,让你彻底掌握客户端状态与服务器状态的协同管理之道。
读完本文你将学到:
- 如何用Zustand构建类型安全的客户端状态管理系统
- TanStack Query在服务器状态同步中的最佳实践
- 两种状态管理方案的边界划分与协同策略
- 大型应用中的状态设计模式与性能优化技巧
状态管理现状分析
现代Web应用开发中,状态管理始终是核心难题。根据Supermemory的业务场景,我们面临着以下具体挑战:
| 状态类型 | 特点 | 传统解决方案 | 存在问题 |
|---|---|---|---|
| 客户端UI状态 | 高频更新、与用户交互强相关、需持久化 | Redux + localStorage | 样板代码过多、性能开销大 |
| 服务器数据状态 | 需缓存、依赖网络、有过期策略 | 自定义hooks + 手动缓存 | 缓存策略复杂、数据一致性难保证 |
| 临时计算状态 | 依赖前两种状态、无需持久化 | 组件内部state | 状态共享困难、逻辑分散 |
Supermemory作为一款知识管理工具,用户期望无缝的离线体验和实时的数据同步,这要求我们必须重新思考状态管理架构。
Zustand:轻量级客户端状态管理
Zustand核心优势
Zustand作为一款轻量级状态管理库,在Supermemory中承担了客户端UI状态的管理职责。与传统方案相比,它具有以下优势:
- 无Provider设计:无需像Redux或Context API那样包裹Provider层级
- 类型安全:原生支持TypeScript,状态定义即类型定义
- 中间件机制:通过中间件轻松实现持久化、日志等功能
- 细粒度订阅:组件可精确订阅所需状态,减少重渲染
Supermemory中的Zustand实践
1. 持久化对话状态管理
在apps/web/stores/chat.ts中,Supermemory使用Zustand结合persist中间件实现了对话记录的持久化:
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface ConversationRecord {
messages: UIMessage[];
title?: string;
lastUpdated: string;
}
export const usePersistentChatStore = create<ConversationsStoreState>()(
persist(
(set, get) => ({
byProject: {},
setConversation(projectId, chatId, messages) {
const now = new Date().toISOString();
set((state) => {
const project = state.byProject[projectId] ?? {
currentChatId: null,
conversations: {},
};
// 状态更新逻辑...
return {
byProject: {
...state.byProject,
[projectId]: {
currentChatId: project.currentChatId,
conversations: {
...project.conversations,
[chatId]: record,
},
},
},
};
});
},
// 其他状态方法...
}),
{
name: "supermemory-chats", // localStorage的key
},
),
);
这个设计巧妙地解决了多项目对话状态的隔离与持久化问题,每个项目的对话历史独立存储,并通过projectId进行隔离。
2. 临时UI状态管理
对于不需要持久化的临时UI状态,如记忆图谱的高亮状态,Supermemory采用了基础的Zustand store设计:
// apps/web/stores/highlights.ts
import { create } from "zustand";
interface GraphHighlightsState {
documentIds: string[];
lastUpdated: number;
setDocumentIds: (ids: string[]) => void;
clear: () => void;
}
export const useGraphHighlightsStore = create<GraphHighlightsState>()(
(set, get) => ({
documentIds: [],
lastUpdated: 0,
setDocumentIds: (ids) => {
const next = Array.from(new Set(ids));
const prev = get().documentIds;
// 避免不必要的重渲染
if (
prev.length === next.length &&
prev.every((id) => next.includes(id))
) {
return;
}
set({ documentIds: next, lastUpdated: Date.now() });
},
clear: () => set({ documentIds: [], lastUpdated: Date.now() }),
}),
);
注意这里的性能优化:通过比较前后状态,避免不必要的set操作,减少组件重渲染。
3. 模块化状态组织
Supermemory将状态按功能域划分为多个store,如chat.ts、highlights.ts、index.ts(项目状态),每个store专注于管理特定领域的状态:
// apps/web/stores/index.ts 中的多store组织
export const useProjectStore = create<ProjectState>()(/* ... */);
export const useMemoryGraphStore = create<MemoryGraphState>()(/* ... */);
export const useChatStore = create<ChatState>()(/* ... */);
// 自定义hooks封装状态访问
export function useProject() {
const selectedProject = useProjectStore((state) => state.selectedProject);
const setSelectedProject = useProjectStore(
(state) => state.setSelectedProject,
);
return { selectedProject, setSelectedProject };
}
这种模块化设计使状态管理更加清晰,每个组件可以只引入所需的状态片段。
TanStack Query:服务器状态管理的最佳实践
TanStack Query核心价值
TanStack Query(原React Query)在Supermemory中负责处理服务器状态,它解决了传统数据获取方案中的诸多痛点:
- 自动缓存与失效:智能管理服务器数据缓存,减少重复请求
- 后台同步:支持在后台自动同步数据,保持UI与服务器一致
- 乐观更新:提升用户体验,先更新UI再等待服务器确认
- 分页与无限滚动:简化复杂数据加载模式的实现
Supermemory中的TanStack Query实践
1. QueryClient配置
Supermemory在应用入口处配置了全局的QueryClient:
// packages/lib/query-client.tsx
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import { useState } from "react"
export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
refetchIntervalInBackground: false,
refetchOnWindowFocus: false,
staleTime: 60 * 1000, // 数据保鲜时间60秒
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
这里的关键配置是staleTime: 60 * 1000,表示数据在60秒内不会被视为过期,避免了不必要的重复请求。
2. 服务器状态查询
Supermemory将常用的服务器数据查询封装为自定义hooks:
// packages/lib/queries.ts
import { useQuery } from "@tanstack/react-query"
import type { useCustomer } from "autumn-js/react"
export const fetchSubscriptionStatus = (
autumn: ReturnType<typeof useCustomer>,
) =>
useQuery({
queryFn: async () => {
const allPlans = [
"pro",
"memory_starter",
"memory_growth",
"consumer_pro",
]
const statusMap: Record<string, boolean | null> = {}
await Promise.all(
allPlans.map(async (plan) => {
try {
const res = await autumn.check({
productId: plan,
})
statusMap[plan] = res.data?.allowed ?? false
} catch (error) {
console.error(`Error checking status for ${plan}:`, error)
statusMap[plan] = false
}
}),
)
return statusMap
},
queryKey: ["subscription-status"],
refetchInterval: 5000, // 每5秒轮询一次
staleTime: 4000, // 4秒后视为过期
})
这个查询定期检查用户的订阅状态,为功能权限控制提供数据支持。
3. 数据突变(Mutations)处理
对于修改服务器数据的操作,Supermemory使用useMutation结合缓存更新策略:
// apps/web/hooks/use-project-mutations.ts
"use client";
import { $fetch } from "@lib/api";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useProject } from "@/stores";
export function useProjectMutations() {
const queryClient = useQueryClient();
const { selectedProject, setSelectedProject } = useProject();
const createProjectMutation = useMutation({
mutationFn: async (name: string) => {
const response = await $fetch("@post/projects", {
body: { name },
});
if (response.error) {
throw new Error(response.error?.message || "Failed to create project");
}
return response.data;
},
onSuccess: (data) => {
toast.success("Project created successfully!");
queryClient.invalidateQueries({ queryKey: ["projects"] });
// 自动切换到新创建的项目
if (data?.containerTag) {
setSelectedProject(data.containerTag);
}
},
onError: (error) => {
toast.error("Failed to create project", {
description: error instanceof Error ? error.message : "Unknown error",
});
},
});
// 其他mutation...
return {
createProjectMutation,
deleteProjectMutation,
switchProject,
};
}
这里的关键是在mutation成功后调用queryClient.invalidateQueries,触发相关查询的重新获取,确保UI数据与服务器同步。
Zustand与TanStack Query的协同策略
明确的状态边界划分
Supermemory团队通过长期实践,总结出清晰的状态划分原则:
具体到代码实现:
-
Zustand负责:
- 当前选中的项目(
selectedProject) - 对话窗口的打开/关闭状态
- 记忆图谱的位置和高亮状态
- 持久化的对话历史记录
- 当前选中的项目(
-
TanStack Query负责:
- 项目列表及其元数据
- 文档和记忆条目数据
- 用户订阅状态和权限
- 服务器端计算的聚合数据
数据流向与交互
两种状态管理方案并非完全独立,它们通过以下方式协同工作:
组件中的协同使用模式
在组件层面,Supermemory形成了固定的状态使用模式:
// 典型组件中的状态使用模式
function MemoryListView() {
// 1. 从Zustand获取客户端状态
const { selectedProject } = useProject();
const { setDocumentIds } = useGraphHighlights();
// 2. 使用TanStack Query获取服务器数据
const { data: documents, isLoading } = useDocumentsQuery(selectedProject);
// 3. 处理用户交互,更新Zustand状态
const handleDocumentClick = (document) => {
setDocumentIds([document.id]);
// 可能同时触发TanStack Query的mutation
updateDocumentMutation.mutate(document.id);
};
// 4. 渲染UI
if (isLoading) return <LoadingSpinner />;
return (
<div>
{documents.map(doc => (
<DocumentCard
key={doc.id}
document={doc}
onClick={() => handleDocumentClick(doc)}
isHighlighted={selectedDocumentIds.includes(doc.id)}
/>
))}
</div>
);
}
这种模式清晰分离了不同类型的状态,使组件逻辑更易于理解和维护。
共享依赖状态的处理
当两种状态存在依赖关系时,Supermemory采用"以Zustand状态作为TanStack Query查询参数"的模式:
// 使用Zustand状态作为查询参数
function useDocumentsQuery(projectId: string) {
return useQuery({
queryKey: ["documents", projectId], // projectId来自Zustand
queryFn: () => fetchDocuments(projectId),
});
}
// 在组件中组合使用
function DocumentsComponent() {
const { selectedProject } = useProject(); // 来自Zustand
const { data: documents } = useDocumentsQuery(selectedProject); // 作为查询参数
// ...
}
当selectedProject变化时,TanStack Query会自动取消旧的请求并发起新的请求,完美处理了状态依赖问题。
性能优化策略
Zustand性能优化
- 精确选择状态片段:
// 不佳: 选择整个状态对象
const state = useChatStore();
// 良好: 只选择需要的状态片段
const isOpen = useChatStore(state => state.isOpen);
- 避免不必要的状态更新:
// highlights.ts中避免不必要的更新
setDocumentIds: (ids) => {
const next = Array.from(new Set(ids));
const prev = get().documentIds;
// 比较前后状态,避免相同状态触发更新
if (
prev.length === next.length &&
prev.every((id) => next.includes(id))
) {
return;
}
set({ documentIds: next, lastUpdated: Date.now() });
}
TanStack Query性能优化
- 合理设置staleTime和cacheTime:
// 查询配置示例
useQuery({
queryKey: ["subscription-status"],
queryFn: fetchSubscriptionStatus,
refetchInterval: 5000, // 数据变化频率高,轮询间隔短
staleTime: 4000, // 略小于轮询间隔,确保数据不过期
cacheTime: 30000, // 缓存保留30秒
})
- 使用查询预取(prefetching):
// 在用户可能访问的地方预取数据
function ProjectList() {
const queryClient = useQueryClient();
return (
<div>
{projects.map(project => (
<div
key={project.id}
onClick={() => {
// 预取该项目的文档数据
queryClient.prefetchQuery({
queryKey: ["documents", project.id],
queryFn: () => fetchDocuments(project.id)
});
navigateToProject(project.id);
}}
>
{project.name}
</div>
))}
</div>
);
}
最佳实践总结
状态设计模式
-
模块化Store设计:
- 按业务功能划分store,避免单一巨大store
- 每个store专注于特定领域的状态管理
- 提供封装良好的自定义hooks访问状态
-
状态访问封装:
// 推荐: 封装状态访问逻辑 export function useProject() { const selectedProject = useProjectStore((state) => state.selectedProject); const setSelectedProject = useProjectStore( (state) => state.setSelectedProject, ); return { selectedProject, setSelectedProject }; } -
乐观更新策略:
useMutation({ mutationFn: updateDocument, onMutate: async (newDoc) => { // 取消当前查询 await queryClient.cancelQueries({ queryKey: ["document", newDoc.id] }); // 保存当前数据 const previousDoc = queryClient.getQueryData(["document", newDoc.id]); // 乐观更新 queryClient.setQueryData(["document", newDoc.id], newDoc); // 返回上下文 return { previousDoc }; }, onError: (err, newDoc, context) => { // 出错时回滚 queryClient.setQueryData( ["document", newDoc.id], context?.previousDoc ); }, onSettled: (doc) => { // 无论成功失败都重新验证 queryClient.invalidateQueries({ queryKey: ["document", doc.id] }); }, });
调试与开发体验
-
使用React Query Devtools:
// query-client.tsx中已经集成 <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> -
Zustand Devtools集成:
import { devtools } from 'zustand/middleware' export const useChatStore = create<ChatState>()( devtools( (set) =>
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



