Skip to content

Composables Overview

Relevant source files

The following files were used as context for generating this wiki page:

This page provides an overview of KanStack's composable architecture, explaining how Vue Composition API composables are used to separate concerns and manage application state. Each composable is responsible for a specific domain, and they work together to provide the complete functionality of the application.

For detailed documentation on individual composables, see:

For information about how these composables are orchestrated in the main application component, see Main Application Component.

Composable Architecture Pattern

KanStack uses Vue's Composition API to organize frontend logic into focused, reusable composables. Each composable is a TypeScript function that returns reactive state and methods, encapsulating a specific aspect of application behavior. This pattern provides:

  • Separation of Concerns: Each composable handles a single domain (workspace state, board actions, card editing, etc.)
  • Testability: Composables are pure functions that can be tested independently
  • Reusability: Composables can be shared across components
  • Type Safety: Full TypeScript support with typed inputs and outputs
  • Reactive State: Built on Vue's reactivity system using ref, computed, and shallowRef

Composable Architecture Overview

Sources: src/App.vue:1-64, src/composables/useWorkspace.ts:1-50, src/composables/useBoardActions.ts:1-50

Core Composables

KanStack implements five primary composables, each with distinct responsibilities:

ComposablePurposeKey StateKey OperationsFile
useWorkspaceWorkspace state and navigationworkspace, currentBoard, selectedCard, boardLineageopenWorkspace, loadWorkspace, selectBoard, selectCard, applyWorkspaceMutationsrc/composables/useWorkspace.ts
useBoardActionsBoard and card mutationsisCreatingCard, isMovingCard, isCreatingColumncreateCard, moveCard, archiveCards, addColumn, renameColumn, deleteColumnsrc/composables/useBoardActions.ts
useBoardSelectionMulti-select card stateselectedCards, selectedKeys, selectedCounthandleSelection, moveSelection, clearSelection, selectSingleReferenced in src/App.vue:60
useCardEditorCard editing sessioneditSession, draftContent, isDirty, isSavingstartEdit, saveCard, deleteCard, autosaveReferenced in src/App.vue
useActionHistoryUndo/redo functionalityundoStack, redoStackpush, shiftUndo, shiftRedo, clearsrc/App.vue:13-15,64

Sources: src/App.vue:10-64, src/composables/useWorkspace.ts:41-555, src/composables/useBoardActions.ts:50-434

Composable Instantiation and Configuration

Composables are instantiated in App.vue and configured with appropriate dependencies:

useWorkspace Instantiation

typescript
const {
    workspace,
    currentBoard,
    currentBoardSlug,
    boardLineage,
    childBoards,
    selectedCard,
    selectedCardSlug,
    selectedCardSourceBoard,
    isLoading,
    errorMessage,
    viewPreferences,
    openWorkspace,
    selectBoard,
    selectCard,
    closeCard,
    applyWorkspaceMutation,
    updateViewPreferences,
} = useWorkspace();

The useWorkspace composable returns reactive state and methods that manage the entire workspace lifecycle. It maintains the loaded workspace, current board selection, and card selection state.

useBoardActions Instantiation with Dependencies

typescript
const appBoardActions = useBoardActions({
    getBoardsBySlug: () => workspace.value?.boardsBySlug ?? {},
    getWorkspaceRoot: () => workspace.value?.rootPath ?? null,
    getBoardFilesBySlug: () => workspace.value?.boardFilesBySlug ?? {},
    getCardsBySlug: () => workspace.value?.cardsBySlug ?? {},
});

The useBoardActions composable requires access to workspace data through dependency injection. This pattern keeps the composable testable and decoupled from global state.

Sources: src/App.vue:29-59, src/composables/useWorkspace.ts:529-554, src/composables/useBoardActions.ts:43-59

Data Flow Pattern

Operation Execution Flow

This sequence shows the standard pattern for tracked operations:

  1. Capture State: Create a snapshot of current state for undo functionality
  2. Execute Operation: Call composable method to perform the operation
  3. Serialize Changes: Generate updated markdown content
  4. Persist to Backend: Invoke Tauri command to save changes
  5. Record History: Store before/after snapshots in action history
  6. Apply Mutation: Update reactive workspace state with new snapshot
  7. UI Update: Vue reactivity triggers component re-renders

Sources: src/App.vue:99-155,554-610, src/composables/useBoardActions.ts:60-81

State Mutation Pattern

All workspace mutations follow a consistent pattern implemented in App.vue:

Tracked Action Pattern

The executeTrackedAction function (src/App.vue:134-155) wraps all operations that should be undoable. It:

  1. Captures the current state snapshot (board, card selection, column selection)
  2. Executes the provided operation function
  3. Records both before and after states in history
  4. Returns the after state for further processing

History State Snapshot Structure

typescript
interface HistoryStateSnapshot {
    currentBoardSlug: string | null;
    selectedCard: {
        slug: string;
        sourceBoardSlug: string;
    } | null;
    selectedColumnSlug: string | null;
    snapshot: WorkspaceSnapshot;
}

Sources: src/App.vue:99-155,211-227, src/history/useActionHistory.ts

Composable Interaction Patterns

Inter-Composable Communication

Composables interact through well-defined interfaces:

  • useWorkspace: The single source of truth for workspace data. Other composables read from it but don't directly modify it.
  • useBoardActions: Reads workspace data through dependency injection, performs operations, returns WorkspaceSnapshot results.
  • useBoardSelection: Maintains its own selection state, independent of workspace state, synchronized via events.
  • useCardEditor: Operates on card data, emits mutations back through events or direct method calls.
  • useActionHistory: Stores snapshots but doesn't read or modify workspace directly.

Sources: src/App.vue:29-64, src/composables/useWorkspace.ts:420-443, src/composables/useBoardActions.ts:43-59

Reactive State Management

All composables use Vue's reactivity primitives for efficient updates:

PrimitiveUsageExample
shallowRefFor objects where only the reference changesworkspace = shallowRef<LoadedWorkspace | null>(null)
refFor primitive values that change frequentlyisLoading = shallowRef(false)
computedFor derived state based on other reactive valuescurrentBoard = computed(() => workspace.value?.boardsBySlug[currentBoardSlug.value])
watchFor side effects when reactive values changewatch(currentBoard, (board) => {...})

Example: Computed Derived State

The currentBoard computed property (src/composables/useWorkspace.ts:57-63) automatically updates when either workspace or currentBoardSlug changes:

typescript
const currentBoard = computed<KanbanBoardDocument | null>(() => {
    if (!workspace.value || !currentBoardSlug.value) {
        return null;
    }
    return workspace.value.boardsBySlug[currentBoardSlug.value] ?? null;
});

This pattern eliminates manual state synchronization and reduces bugs.

Sources: src/composables/useWorkspace.ts:41-79, src/composables/useBoardActions.ts:50-58

App.vue Orchestration

App.vue serves as the orchestration layer that coordinates all composables. Key responsibilities:

Orchestration Responsibilities

ResponsibilityImplementationLines
Composable InitializationInstantiate and configure all composablessrc/App.vue:29-64
Event HandlingConvert UI events into composable method callssrc/App.vue:420-610
Keyboard ShortcutsHandle global keyboard events and dispatch to composablessrc/App.vue:912-1108
Menu ActionsListen for menu events from backend and route to handlerssrc/App.vue:1122-1201
History IntegrationWrap operations in executeTrackedAction for undo/redosrc/App.vue:134-155
State SynchronizationClear selections when changing contextssrc/App.vue:424-428
Error HandlingDisplay error messages and manage loading statessrc/App.vue:61,873-880

Example: Keyboard Shortcut Routing

typescript
function handleGlobalKeydown(event: KeyboardEvent) {
    // Undo/Redo
    if (hasPrimaryModifier && event.key.toLowerCase() === "z") {
        void undoAction();  // -> useActionHistory
    }
    
    // Create card
    if (hasPrimaryModifier && event.key.toLowerCase() === "n") {
        void createCardFromBoard();  // -> useBoardActions
    }
    
    // Navigate selection
    if (event.key === "ArrowRight") {
        boardSelection.moveSelection("right");  // -> useBoardSelection
    }
}

Sources: src/App.vue:912-1108,1135-1201

Composable Lifecycle Hooks

Composables use Vue lifecycle hooks for initialization and cleanup:

useWorkspace Lifecycle

  • onMounted: Attach workspace change listener, restore previous workspace
  • onUnmounted: Clean up event listeners, stop file watching, cancel pending config writes

Lifecycle Hook Pattern

typescript
onMounted(() => {
    void attachWorkspaceListener();  // Listen for backend file changes
});

onUnmounted(() => {
    if (pendingConfigWrite !== null) {
        window.clearTimeout(pendingConfigWrite);
    }
    if (unlistenWorkspaceChanges) {
        unlistenWorkspaceChanges();  // Clean up event listener
    }
    void stopWorkspaceWatch();  // Stop backend file watcher
});

This ensures proper resource management and prevents memory leaks.

Sources: src/composables/useWorkspace.ts:511-527, src/App.vue:193-209

Summary

KanStack's composable architecture provides:

  1. Clear Separation: Each composable has a single, well-defined responsibility
  2. Testability: Pure functions with dependency injection
  3. Type Safety: Full TypeScript support throughout
  4. Reactive Updates: Automatic UI updates via Vue reactivity
  5. Undo/Redo: Consistent state snapshot pattern for all operations
  6. Orchestration: App.vue coordinates composables without complex logic

For implementation details of individual composables, see:

Sources: src/App.vue:1-1714, src/composables/useWorkspace.ts:1-630, src/composables/useBoardActions.ts:1-449