My App
BoardKitFrontend

BoardStore

Mutable store with slice-based pub/sub for high-performance state management

BoardStore is a mutable state container that lives outside React's state management. It uses a slice-based publish/subscribe pattern, and hooks bridge it into React via useSyncExternalStore.

Why Not React State?

During rapid drawing (60+ pointer events per second), React's setState / useReducer would trigger excessive re-renders. By keeping the hot path (scene mutations, viewport updates) in a mutable store and only notifying relevant slices, the canvas renderer can update independently of React's reconciliation cycle.

BoardStoreState

interface BoardStoreState {
    board: Board | null;
    pages: Page[];
    activePageId: string | null;
    scene: SceneState;
    viewport: ViewportState;
    activeTool: string;
    toolConfig: Record<string, unknown>;
    selectedIds: Set<string>;
    hoveredId: string | null;
    history: History;
    participants: Map<string, Participant>;
    cursors: Map<string, CursorPosition>;
    lastError: BoardError | null;
    gridConfig: GridConfig;
}

Store Slices

Each state mutation notifies a specific slice. Hooks subscribe to only the slices they need.

type StoreSlice =
    | 'board'
    | 'pages'
    | 'scene'
    | 'viewport'
    | 'tool'
    | 'selection'
    | 'participants'
    | 'cursors'
    | 'error'
    | 'grid';
SliceNotified By
boardsetBoard()
pagessetPages(), setActivePageId()
sceneupdateScene()
viewportupdateViewport()
toolsetActiveTool(), setToolConfig()
selectionsetSelection(), setHoveredId()
participantssetParticipants()
cursorsupdateCursor(), removeCursor()
errorsetError()
gridsetGridConfig()

Methods

State Access

getState(): BoardStoreState;

Board Data

setBoard(board: Board | null): void;
setPages(pages: Page[]): void;
setActivePageId(pageId: string | null): void;

Scene

updateScene(fn: (scene: SceneState) => SceneState): void;

The update function receives the current scene and must return a new scene. This is where all element mutations happen.

Viewport

updateViewport(fn: (vp: ViewportState) => ViewportState): void;

Tool

setActiveTool(toolId: string): void;
setToolConfig(config: Record<string, unknown>): void;

Selection

setSelection(ids: Set<string>): void;
setHoveredId(id: string | null): void;

Collaboration

setParticipants(participants: Map<string, Participant>): void;
updateCursor(userId: string, cursor: CursorPosition): void;
removeCursor(userId: string): void;

Grid

setGridConfig(config: Partial<GridConfig>): void;

Error

setError(error: BoardError | null): void;

Subscriptions

subscribe(slice: string, listener: () => void): () => void;

Returns an unsubscribe function. The listener is called synchronously whenever the specified slice is notified.

Usage from Hooks

// This is how hooks bridge the store into React:
import { useSyncExternalStore } from 'react';

function useGridConfig() {
    const { store } = useBoardKit();

    return useSyncExternalStore(
        (cb) => store.subscribe('grid', cb),
        () => store.getState().gridConfig,
    );
}

On this page