Implementing Data Caching in Zustand Store
Β· 3 min read
Encountered this issue while building an AI Agent platform. Here's the root cause and solution.
TL;DRβ
Add lastFetchTime field and TTL constant to Zustand store. Check cache expiration before requesting. Implement effective data caching in ~10 lines of code, avoiding duplicate requests across pages.
Problemβ
MCP tools list is used by multiple pages (Agent Settings, Tools Marketplace, Chat tool selector). Each page entry triggers an API request:
GET /api/mcp-tools β 200 (AgentSettingsPage)
GET /api/mcp-tools β 200 (McpToolsPage)
GET /api/mcp-tools β 200 (ChatPage tool selector)
Tools list rarely changes (admin configures manually), but frequent requests waste bandwidth and slow page loads.
Root Causeβ
Components call API directly without caching:
// β No caching: requests on every mount
function McpToolsPage() {
const [tools, setTools] = useState([])
useEffect(() => {
mcpToolsApi.list().then(setTools)
}, [])
return <ToolList tools={tools} />
}
Problems:
- Each page requests independently β No global state sharing
- Repeated requests in short time β Triggered when user navigates between pages
- No refresh control β Re-fetches even when data unchanged
Solutionβ
Zustand Store + TTL Cacheβ
// src/stores/mcpToolsStore.ts
import { create } from 'zustand'
import { mcpToolsApi, type McpTool } from '@/services/api'
const CACHE_TTL = 10 * 60 * 1000 // 10 minutes
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()
// Has cache, not expired, not forced β skip
if (tools.length && lastFetchTime && !force) {
if (Date.now() - lastFetchTime < CACHE_TTL) {
return // Cache hit
}
}
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 }),
}))
Component Usageβ
// β
Using store cache
function McpToolsPage() {
const { tools, loading, fetchTools } = useMcpToolsStore()
useEffect(() => {
fetchTools() // Auto-checks cache
}, [fetchTools])
if (loading) return <Spinner />
return <ToolList tools={tools} />
}
// Force refresh
function RefreshButton() {
const { fetchTools } = useMcpToolsStore()
return <button onClick={() => fetchTools(true)}>Refresh</button>
}
Core Logic Explainedβ
// Cache check logic
if (tools.length && lastFetchTime && !force) {
if (Date.now() - lastFetchTime < CACHE_TTL) {
return // Cache valid, skip request
}
}
| Condition | Meaning |
|---|---|
tools.length | Has data (empty array not valid cache) |
lastFetchTime | Recorded last request time |
!force | Not forced refresh |
Date.now() - lastFetchTime < CACHE_TTL | Not expired |
Use Casesβ
| Scenario | Suitable | Reason |
|---|---|---|
| Tool lists, config dictionaries | β Yes | Low change frequency, shared across pages |
| User permissions, roles | β Yes | Rarely changes within session |
| Real-time data (messages, notifications) | β No | Needs latest state |
| Paginated lists | β No | Large data volume, complex caching strategy |
Extension: Fine-grained Cache Controlβ
interface CacheOptions {
ttl: number // Expiration time
staleWhileRevalidate: boolean // Return stale data while refreshing
}
// Background refresh after expiration, return cached data first
if (tools.length && lastFetchTime) {
const age = Date.now() - lastFetchTime
if (age < CACHE_TTL) {
return // Cache fresh
}
if (options.staleWhileRevalidate && age < CACHE_TTL * 2) {
// Cache stale but acceptable, background refresh
mcpToolsApi.list().then(data => set({ tools: data, lastFetchTime: Date.now() }))
return
}
}
Key Principlesβ
- Set reasonable TTL β Based on data change frequency
- Provide force refresh β Users can manually get latest data
- Show loading on first fetch β Empty data shouldn't skip request
Interested in similar solutions? Contact us