State Management Guide
Managing application state with Zustand.
Overview
IfAI uses Zustand for state management - a lightweight, performant alternative to Redux.
Why Zustand?
- Minimal boilerplate: Less code than Redux
- TypeScript first: Excellent TS support
- Performance: React selectors avoid re-renders
- Simple API: Easy to learn and use
Store Structure
stores/
├── useChatStore.ts # Chat state & AI interactions
├── useFileStore.ts # File operations
├── useEditorStore.ts # Editor configuration
├── useAgentStore.ts # Agent execution state
├── useSettingsStore.ts # User preferences
└── useUIStore.ts # UI state (panels, modals)Creating a Store
Basic Store
typescript
import { create } from 'zustand'
interface CounterStore {
count: number
increment: () => void
decrement: () => void
}
export const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))Using Actions
typescript
function Counter() {
const { count, increment, decrement } = useCounterStore()
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}Advanced Patterns
Async Actions
typescript
interface ChatStore {
messages: Message[]
loading: boolean
error: string | null
// Actions
sendMessage: (message: string) => Promise<void>
clearHistory: () => void
}
export const useChatStore = create<ChatStore>((set, get) => ({
messages: [],
loading: false,
error: null,
sendMessage: async (message: string) => {
set({ loading: true, error: null })
try {
const response = await aiClient.chat([...get().messages, {
role: 'user',
content: message
}])
set((state) => ({
messages: [...state.messages, { role: 'user', content: message }],
loading: false
}))
// Add assistant response
set((state) => ({
messages: [...state.messages, {
role: 'assistant',
content: response
}]
}))
} catch (error) {
set({
error: error.message,
loading: false
})
}
},
clearHistory: () => set({ messages: [] })
}))Selectors
typescript
// Store with selectors
interface TodoStore {
items: Todo[]
// Selectors
completedCount: () => number
pendingItems: () => Todo[]
}
export const useTodoStore = create<TodoStore>((set, get) => ({
items: [],
completedCount: () => {
return get().items.filter(item => item.completed).length
},
pendingItems: () => {
return get().items.filter(item => !item.completed)
}
}))
// Using selectors
function TodoStats() {
const completedCount = useTodoStore((state) => state.completedCount())
return <div>Completed: {completedCount}</div>
}Combining Stores
typescript
// Root store that combines multiple stores
interface RootStore {
chat: ChatStore
editor: EditorStore
file: FileStore
}
// Use multiple stores in component
function MyComponent() {
const messages = useChatStore((state) => state.messages)
const currentFile = useFileStore((state) => state.currentFile)
const fontSize = useEditorStore((state) => state.fontSize)
return <div>{/* ... */}</div>
}Store Persistence
Persisting to LocalStorage
typescript
import { persist } from 'zustand/middleware'
interface SettingsStore {
theme: 'light' | 'dark'
fontSize: number
setTheme: (theme: 'light' | 'dark') => void
setFontSize: (size: number) => void
}
export const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
theme: 'dark',
fontSize: 14,
setTheme: (theme) => set({ theme }),
setFontSize: (fontSize) => set({ fontSize })
}),
{
name: 'ifai-settings', // LocalStorage key
}
)
)File System Persistence
typescript
import { TauriStorage } from '@/utils/storage'
interface FileStore {
recentFiles: string[]
addRecentFile: (path: string) => void
}
export const useFileStore = create<FileStore>()(
persist(
(set) => ({
recentFiles: [],
addRecentFile: (path) => set((state) => ({
recentFiles: [path, ...state.recentFiles.slice(0, 9)]
}))
}),
{
name: 'ifai-files',
storage: TauriStorage // Custom storage adapter
}
)
)Best Practices
1. Keep Stores Focused
typescript
// Good: Focused store for chat
interface ChatStore {
messages: Message[]
sendMessage: (msg: string) => Promise<void>
}
// Avoid: Bloated store with everything
interface AppStore {
messages: Message[]
files: File[]
settings: Settings
editor: EditorState
ui: UIState
// ... too many concerns
}2. Use Selectors for Performance
typescript
// Bad: Re-renders on every state change
function MyComponent() {
const store = useChatStore()
return <div>{store.messages.length}</div>
}
// Good: Only re-renders when messages change
function MyComponent() {
const messageCount = useChatStore((state) => state.messages.length)
return <div>{messageCount}</div>
}3. TypeScript Best Practices
typescript
// Define types for actions
interface ChatActions {
sendMessage: (message: string) => Promise<void>
clearHistory: () => void
}
// Separate state from actions
interface ChatState {
messages: Message[]
loading: boolean
}
type ChatStore = ChatState & ChatActions
// Create store with types
export const useChatStore = create<ChatStore>((set, get) => ({
messages: [],
loading: false,
sendMessage: async (message) => { /* ... */ },
clearHistory: () => { /* ... */ }
}))4. DevTools Integration
typescript
import { devtools } from 'zustand/middleware'
export const useChatStore = create<ChatStore>()(
devtools(
(set, get) => ({
messages: [],
// ...
}),
{ name: 'ChatStore' }
)
)Testing Stores
Unit Testing
typescript
import { act } from 'react'
import { renderHook } from '@testing-library/react'
describe('useChatStore', () => {
it('sends message', async () => {
const { result } = renderHook(() => useChatStore())
await act(async () => {
await result.current.sendMessage('Hello')
})
expect(result.current.messages).toHaveLength(1)
})
})Common Patterns
Loading State
typescript
interface LoadingState {
data: T | null
loading: boolean
error: string | null
fetchData: () => Promise<void>
}
export const createDataStore = <T>() => {
return create<LoadingState<T>>((set, get) => ({
data: null,
loading: false,
error: null,
fetchData: async () => {
set({ loading: true, error: null })
try {
const data = await fetchData()
set({ data, loading: false })
} catch (error) {
set({ error: error.message, loading: false })
}
}
}))
}Pagination
typescript
interface PaginatedState<T> {
items: T[]
page: number
pageSize: number
total: number
nextPage: () => void
prevPage: () => void
}
export const createPaginatedStore = <T>() => {
return create<PaginatedState<T>>((set, get) => ({
items: [],
page: 1,
pageSize: 20,
total: 0,
nextPage: () => set((state) => ({
page: state.page + 1
})),
prevPage: () => set((state) => ({
page: Math.max(1, state.page - 1)
}))
}))
}Next Steps
- Frontend Guide - Using stores in components
- Component Library - Available components
- API Reference - Store integration