Skip to content

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

Released under the MIT License.