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[]) => voidTask 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[]) => voidBoard Actions
// Add board
addBoard: (board: Partial<Board>) => void
// Update board
updateBoard: (boardId: string, updates: Partial<Board>) => void
// Delete board
deleteBoard: (boardId: string) => voidColumn 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[]) => voidNotification Actions
// Mark notification read
markNotificationRead: (notificationId: string) => void
// Mark all read
markAllNotificationsRead: () => void
// Add notification
addNotification: (notification: Partial<Notification>) => voidTime 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) => voidStore 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.length2. 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!