Architecture
State Management

State Management

Sunday uses Zustand for state management - a minimal, fast state-management library for React.

Why Zustand?

  • Simple API - No boilerplate, no providers needed
  • Small bundle - ~1KB gzipped
  • Fast - Optimized re-renders
  • TypeScript - First-class TypeScript support
  • Flexible - Works with React and vanilla JS

Store Structure

The main store is defined in lib/store.ts and contains:

interface AppState {
  // UI State
  sidebarCollapsed: boolean
  currentView: ViewType
  selectedBoardId: string | null
  selectedTaskId: string | null
  searchQuery: string
  showAutomationsPanel: boolean
  showFormsPanel: boolean
  showActivityLog: boolean
  globalSearchOpen: boolean
  activeFilters: FilterCondition[]
  sortConfig: SortConfig[]
  
  // Data
  workspaces: Workspace[]
  boards: Board[]
  tasks: Task[]
  users: User[]
  currentUser: User | null
  notifications: Notification[]
  automations: Automation[]
  forms: Form[]
  activityLog: ActivityLogEntry[]
  savedFilters: SavedFilter[]
  activeTimers: Record<string, number>
  userSettings: UserSettings
  
  // Actions...
}

Using the Store

Basic Usage

import { useAppStore } from '@/lib/store'
 
function MyComponent() {
  // Select specific state
  const tasks = useAppStore((state) => state.tasks)
  const currentView = useAppStore((state) => state.currentView)
  
  // Select actions
  const addTask = useAppStore((state) => state.addTask)
  const updateTask = useAppStore((state) => state.updateTask)
  
  return (
    <button onClick={() => addTask({ name: 'New Task', ... })}>
      Add Task
    </button>
  )
}

Selecting Multiple Values

// Option 1: Multiple selectors (causes re-render if any value changes)
function Component() {
  const { tasks, boards } = useAppStore((state) => ({
    tasks: state.tasks,
    boards: state.boards
  }))
}
 
// Option 2: Shallow comparison (recommended for objects)
import { shallow } from 'zustand/shallow'
 
function Component() {
  const { tasks, boards } = useAppStore(
    (state) => ({ tasks: state.tasks, boards: state.boards }),
    shallow
  )
}

Store Actions

UI Actions

// Toggle sidebar
toggleSidebar: () => void
 
// Change current view
setCurrentView: (view: ViewType) => void
 
// Select board/task
selectBoard: (boardId: string | null) => void
selectTask: (taskId: string | null) => void
 
// Search
setSearchQuery: (query: string) => void
setGlobalSearchOpen: (open: boolean) => void
 
// Filters
setActiveFilters: (filters: FilterCondition[]) => void
setSortConfig: (config: SortConfig[]) => void

Task Actions

// Add task
addTask: (task: Partial<Task>) => void
 
// Update task
updateTask: (taskId: string, updates: Partial<Task>) => void
 
// Delete task
deleteTask: (taskId: string) => void
 
// Move task to different group
moveTask: (taskId: string, newGroupId: string) => void
 
// Duplicate task
duplicateTask: (taskId: string) => void
 
// Add comment
addComment: (taskId: string, content: string, mentions?: string[]) => void

Board Actions

// Add board
addBoard: (board: Partial<Board>) => void
 
// Update board
updateBoard: (boardId: string, updates: Partial<Board>) => void
 
// Delete board
deleteBoard: (boardId: string) => void

Column Actions

// Add column
addColumn: (boardId: string, column: Column) => void
 
// Update column
updateColumn: (boardId: string, columnId: string, updates: Partial<Column>) => void
 
// Delete column
deleteColumn: (boardId: string, columnId: string) => void
 
// Reorder columns
reorderColumns: (boardId: string, columnIds: string[]) => void

Notification Actions

// Mark notification read
markNotificationRead: (notificationId: string) => void
 
// Mark all read
markAllNotificationsRead: () => void
 
// Add notification
addNotification: (notification: Partial<Notification>) => void

Time Tracking Actions

// Start timer
startTimer: (taskId: string) => void
 
// Stop timer
stopTimer: (taskId: string) => void
 
// Add manual time entry
addManualTimeEntry: (taskId: string, duration: number, description?: string) => void

Store Selectors

For complex derived state, create selectors:

// Get tasks for a specific board
const tasksForBoard = useAppStore((state) => 
  state.tasks.filter(t => t.boardId === boardId)
)
 
// Get unread notifications count
const unreadCount = useAppStore((state) => 
  state.notifications.filter(n => !n.read).length
)
 
// Get current board
const currentBoard = useAppStore((state) => 
  state.boards.find(b => b.id === state.selectedBoardId)
)

Async Actions with API

The store includes async methods that interact with the API:

// Create task via API
createTaskAsync: async (data) => {
  const response = await fetch('/api/tasks', {
    method: 'POST',
    body: JSON.stringify(data)
  })
  const task = await response.json()
  
  set((state) => ({
    tasks: [...state.tasks, task]
  }))
  
  return task
}

Best Practices

1. Keep Selectors Small

// ✅ Good - only subscribes to what's needed
const taskCount = useAppStore((state) => state.tasks.length)
 
// ❌ Bad - subscribes to entire tasks array
const tasks = useAppStore((state) => state.tasks)
const taskCount = tasks.length

2. Memoize Expensive Computations

import { useMemo } from 'react'
 
function BoardStats() {
  const tasks = useAppStore((state) => state.tasks)
  
  const stats = useMemo(() => ({
    total: tasks.length,
    done: tasks.filter(t => t.status === 'done').length,
    inProgress: tasks.filter(t => t.status === 'working').length
  }), [tasks])
  
  return <div>{stats.done} / {stats.total}</div>
}

3. Use Actions, Not Direct State Mutation

// ✅ Good - use store action
const updateTask = useAppStore((state) => state.updateTask)
updateTask(taskId, { status: 'done' })
 
// ❌ Bad - never mutate state directly
const tasks = useAppStore((state) => state.tasks)
tasks[0].status = 'done' // This won't trigger re-renders!