Supermemory状态管理策略:Zustand与TanStack Query的协同使用

Supermemory状态管理策略:Zustand与TanStack Query的协同使用

【免费下载链接】supermemory Build your own second brain with supermemory. It's a ChatGPT for your bookmarks. Import tweets or save websites and content using the chrome extension. 【免费下载链接】supermemory 项目地址: https://gitcode.com/GitHub_Trending/su/supermemory

引言:状态管理的双重挑战

你是否还在为前端应用中的状态管理而头疼?当用户数据、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.tshighlights.tsindex.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团队通过长期实践,总结出清晰的状态划分原则:

mermaid

具体到代码实现:

  • Zustand负责

    • 当前选中的项目(selectedProject)
    • 对话窗口的打开/关闭状态
    • 记忆图谱的位置和高亮状态
    • 持久化的对话历史记录
  • TanStack Query负责

    • 项目列表及其元数据
    • 文档和记忆条目数据
    • 用户订阅状态和权限
    • 服务器端计算的聚合数据

数据流向与交互

两种状态管理方案并非完全独立,它们通过以下方式协同工作:

mermaid

组件中的协同使用模式

在组件层面,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性能优化

  1. 精确选择状态片段
// 不佳: 选择整个状态对象
const state = useChatStore();

// 良好: 只选择需要的状态片段
const isOpen = useChatStore(state => state.isOpen);
  1. 避免不必要的状态更新
// 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性能优化

  1. 合理设置staleTime和cacheTime
// 查询配置示例
useQuery({
  queryKey: ["subscription-status"],
  queryFn: fetchSubscriptionStatus,
  refetchInterval: 5000, // 数据变化频率高,轮询间隔短
  staleTime: 4000, // 略小于轮询间隔,确保数据不过期
  cacheTime: 30000, // 缓存保留30秒
})
  1. 使用查询预取(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>
  );
}

最佳实践总结

状态设计模式

  1. 模块化Store设计

    • 按业务功能划分store,避免单一巨大store
    • 每个store专注于特定领域的状态管理
    • 提供封装良好的自定义hooks访问状态
  2. 状态访问封装

    // 推荐: 封装状态访问逻辑
    export function useProject() {
      const selectedProject = useProjectStore((state) => state.selectedProject);
      const setSelectedProject = useProjectStore(
        (state) => state.setSelectedProject,
      );
      return { selectedProject, setSelectedProject };
    }
    
  3. 乐观更新策略

    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] });
      },
    });
    

调试与开发体验

  1. 使用React Query Devtools

    // query-client.tsx中已经集成
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
    
  2. Zustand Devtools集成

    import { devtools } from 'zustand/middleware'
    
    export const useChatStore = create<ChatState>()(
      devtools(
        (set) =>

【免费下载链接】supermemory Build your own second brain with supermemory. It's a ChatGPT for your bookmarks. Import tweets or save websites and content using the chrome extension. 【免费下载链接】supermemory 项目地址: https://gitcode.com/GitHub_Trending/su/supermemory

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值