Use when creating or modifying Zustand stores, reading from stores in components, or working with SyncBatchService. Load for Phase 6.
useShallow mandatory for all multi-field store reads — no exceptionsuseChatStore.setState() directly — only defined actionsfetch('/api/...')SyncBatchService must wrap all rapid mutations to stay under 30 RPM{entity}.store.ts — one file per domain// store/chat.store.ts
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'
import type { Chat } from '@brainbox/shared'
import { logger } from '@/lib/logger'
import { syncBatch } from '@/lib/services/sync-batch.service'
interface ChatStore {
// State
chats: Chat[]
selectedChatId: string | null
isLoading: boolean
// Actions
setChats: (chats: Chat[]) => void
addChat: (chat: Chat) => void
updateChat: (id: string, updates: Partial<Chat>) => Promise<void>
deleteChat: (id: string) => Promise<void>
selectChat: (id: string | null) => void
setLoading: (loading: boolean) => void
}
export const useChatStore = create<ChatStore>((set, get) => ({
chats: [],
selectedChatId: null,
isLoading: false,
setChats: (chats) => set({ chats }),
addChat: (chat) => set(s => ({ chats: [chat, ...s.chats] })),
selectChat: (id) => set({ selectedChatId: id }),
setLoading: (loading) => set({ isLoading: loading }),
updateChat: async (id, updates) => {
const snapshot = get().chats // 1. snapshot
set(s => ({ chats: s.chats.map(c => c.id === id ? { ...c, ...updates } : c) })) // 2. optimistic
await syncBatch.enqueue(`/api/chats/${id}`, 'PUT', updates, id) // 3. debounced API
// rollback handled by syncBatch on failure
},
deleteChat: async (id) => {
const snapshot = get().chats // 1. snapshot
set(s => ({ chats: s.chats.filter(c => c.id !== id) })) // 2. optimistic
const res = await fetch(`/api/chats/${id}`, { method: 'DELETE' })
if (!res.ok) {
set({ chats: snapshot }) // 3. rollback
logger.error('ChatStore', 'Delete failed', { id })
}
},
}))
// ✅ correct — only re-renders when chats or isLoading changes
const { chats, isLoading } = useChatStore(
useShallow(s => ({ chats: s.chats, isLoading: s.isLoading }))
)
// ✅ single field — useShallow optional but fine
const chats = useChatStore(s => s.chats)
// ❌ forbidden — subscribes to entire store, re-renders on any change
const store = useChatStore()
const { chats, isLoading } = useChatStore()
deleteChat: async (id: string): Promise<void> => {
const snapshot = get().chats // 1. Always snapshot first
set(s => ({ chats: s.chats.filter(c => c.id !== id) })) // 2. Apply optimistic immediately
const res = await fetch(`/api/chats/${id}`, { method: 'DELETE' })
if (!res.ok) {
set({ chats: snapshot }) // 3. Rollback on failure
logger.error('ChatStore', 'Delete failed', { id, status: res.status })
}
},
// lib/services/sync-batch.service.ts
interface QueueItem { url: string; method: string; body: unknown; debounceId: string }
class SyncBatchService {
private queue = new Map<string, QueueItem>()
private timer: ReturnType<typeof setTimeout> | null = null
enqueue(url: string, method: string, body: unknown, debounceId: string): void {
this.queue.set(debounceId, { url, method, body, debounceId }) // dedup by id
if (this.timer) clearTimeout(this.timer)
this.timer = setTimeout(() => this.flush(), 50) // 50ms debounce
}
private async flush(): Promise<void> {
const items = Array.from(this.queue.values())
this.queue.clear()
await Promise.allSettled(
items.map(({ url, method, body }) =>
fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
)
)
}
}
export const syncBatch = new SyncBatchService()
// Server Components cannot use Zustand stores directly
// Use Supabase directly in server components or pass data as props
// Zustand is client-side state only
❌ const store = useChatStore() // subscribes to everything
❌ useChatStore.setState({ chats: [] }) // external mutation
❌ supabase.from('chats').select() in store // use fetch('/api/chats')
❌ const { chats } = useChatStore() in Server Component // client-only hook
❌ // optimistic update without rollback // always implement both
useShallow?SyncBatchService?fetch('/api/...') — no direct Supabase?