跳到主要内容

1 篇博文 含有标签「Zustand」

查看所有标签

在 Zustand Store 中实现数据缓存

· 阅读需 3 分钟

在构建 AI Agent 平台时遇到此问题,记录根因与解法。

TL;DR

在 Zustand store 中添加 lastFetchTime 字段和 TTL 常量,请求前检查缓存是否过期。10 行代码实现简单有效的数据缓存,避免多页面重复请求同一数据。

问题现象

MCP 工具列表被多个页面使用(Agent 设置页、工具市场页、聊天页),每次进入页面都触发 API 请求:

GET /api/mcp-tools  → 200  (AgentSettingsPage)
GET /api/mcp-tools → 200 (McpToolsPage)
GET /api/mcp-tools → 200 (ChatPage tool selector)

工具列表变化频率很低(管理员手动配置),频繁请求浪费带宽且影响页面加载速度。

根因

直接在组件中调用 API,没有缓存层:

// ❌ 无缓存:每次挂载都请求
function McpToolsPage() {
const [tools, setTools] = useState([])

useEffect(() => {
mcpToolsApi.list().then(setTools)
}, [])

return <ToolList tools={tools} />
}

问题:

  1. 每个页面独立请求 — 没有全局状态共享
  2. 短时间内重复请求 — 用户在页面间跳转时触发多次
  3. 无法控制刷新频率 — 即使数据未变化也重新获取

解决方案

Zustand Store + TTL 缓存

// src/stores/mcpToolsStore.ts
import { create } from 'zustand'
import { mcpToolsApi, type McpTool } from '@/services/api'

const CACHE_TTL = 10 * 60 * 1000 // 10 分钟

interface McpToolsState {
tools: McpTool[]
lastFetchTime: number | null
loading: boolean
error: string | null

fetchTools: (force?: boolean) => Promise<void>
clearError: () => void
}

export const useMcpToolsStore = create<McpToolsState>((set, get) => ({
tools: [],
lastFetchTime: null,
loading: false,
error: null,

fetchTools: async (force = false) => {
const { tools, lastFetchTime } = get()

// 有缓存且未过期且非强制 → 跳过
if (tools.length && lastFetchTime && !force) {
if (Date.now() - lastFetchTime < CACHE_TTL) {
return // 缓存命中,直接返回
}
}

set({ loading: true, error: null })
try {
const data = await mcpToolsApi.list()
set({ tools: data, lastFetchTime: Date.now(), loading: false })
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to fetch tools'
set({ error: message, loading: false })
}
},

clearError: () => set({ error: null }),
}))

组件中使用

// ✅ 使用 store 缓存
function McpToolsPage() {
const { tools, loading, fetchTools } = useMcpToolsStore()

useEffect(() => {
fetchTools() // 自动检查缓存
}, [fetchTools])

if (loading) return <Spinner />
return <ToolList tools={tools} />
}

// 强制刷新
function RefreshButton() {
const { fetchTools } = useMcpToolsStore()
return <button onClick={() => fetchTools(true)}>刷新</button>
}

核心逻辑解析

// 缓存检查逻辑
if (tools.length && lastFetchTime && !force) {
if (Date.now() - lastFetchTime < CACHE_TTL) {
return // 缓存有效,跳过请求
}
}
条件说明
tools.length已有数据(空数组不算有效缓存)
lastFetchTime记录了上次请求时间
!force非强制刷新
Date.now() - lastFetchTime < CACHE_TTL未超过过期时间

适用场景

场景是否适合原因
工具列表、配置字典✅ 适合变化频率低,多页面共享
用户权限、角色✅ 适合会话内基本不变
实时数据(消息、通知)❌ 不适合需要最新状态
分页列表❌ 不适合数据量大,缓存策略复杂

扩展:更精细的缓存控制

interface CacheOptions {
ttl: number // 过期时间
staleWhileRevalidate: boolean // 过期后先返回旧数据再更新
}

// 过期后后台刷新,先返回缓存数据
if (tools.length && lastFetchTime) {
const age = Date.now() - lastFetchTime
if (age < CACHE_TTL) {
return // 缓存新鲜
}
if (options.staleWhileRevalidate && age < CACHE_TTL * 2) {
// 缓存过期但可接受,后台刷新
mcpToolsApi.list().then(data => set({ tools: data, lastFetchTime: Date.now() }))
return
}
}

关键原则

  1. 缓存时间要合理 — 根据数据变化频率设置 TTL
  2. 提供强制刷新入口 — 用户可手动刷新最新数据
  3. 首次加载要有 loading 状态 — 空数据时不应跳过请求

对类似需求感兴趣?联系合作