Skip to main content

State Management

nque keeps all global state in a single Zustand store composed from domain slices.

The slice pattern​

Each domain owns a slice file in src/store/ that exports:

  1. An interface describing the slice's state and actions.
  2. A create<Domain>Slice factory (StateCreator).
  3. Pure *Impl helper functions containing the actual state transitions.

src/store/root.ts composes them:

type RootSlice = ShowSlice & ModeSlice & PerformSlice & ImportSlice;

const rootStore = create<RootSlice>()((...a) => ({
...createShowSlice(...a),
...createModeSlice(...a),
...createPerformSlice(...a),
...createImportSlice(...a),
}));

export const useShow = (): ShowSlice => rootStore((state) => state);
export const useMode = (): ModeSlice => rootStore((state) => state);
export const usePerform = (): PerformSlice => rootStore((state) => state);
export const useImport = (): ImportSlice => rootStore((state) => state);

Hooks are named use<Domain>.

Why separate *Impl functions​

Slice actions are one-liners that call a pure helper through set, for example:

renameShow: (title) => set((state) => renameShowImpl(state.show, title)),

The helper takes the current data and parameters and returns a partial state update:

export const renameShowImpl: SliceMethod<ShowSlice, ShowSlice["show"], string> = (show, title) => ({ show: { ...show, title }, isDirty: true });

This keeps the transition logic pure, immutable, and trivially unit-testable without touching the store. SliceMethod<TSlice, TState, TParams> in src/store/types.ts types the helper signature.

The slices​

SliceFileResponsibility
ShowSlicestore/show.tsThe show tree, dirty tracking, file path, and all structural edits
ModeSlicestore/mode.tsThe active mode ("edit" or "perform").
PerformSlicestore/perform.tsHighlight/active indices and navigation for perform mode.
ImportSlicestore/import.tsPDF import status, phase, and page progress.

ShowSlice​

Holds show, a savedShow snapshot, isDirty, filePath, and initialized. It exposes creation (newShow, setAsActiveShow, loadShowFile), persistence (markSaved), and tree editing (rename*, add*, remove*, update*). Structural helpers recurse the tree immutably. isDirty is set by edits and cleared by markSaved/loadShowFile.

PerformSlice​

Holds highlightIndex and activeIndex. nextHighlight/previousHighlight take the list of cue indices and move within it; fireCue copies the highlight to the active index; resetCues clears both.

ImportSlice​

Holds status, isImporting, phase, current, total, and error. It is driven by ocr-import-progress events from the backend and by the file-actions hook.

Non-store state​

Ephemeral UI state (open/closed dialogs, input values) stays in component useState. The file-actions hook (src/hooks/use-file-actions.ts) reads and mutates the store imperatively via rootStore.getState() for command flows, and registers global keyboard shortcuts with tinykeys.