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:
- An interface describing the slice's state and actions.
- A
create<Domain>Slicefactory (StateCreator). - Pure
*Implhelper 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
| Slice | File | Responsibility |
|---|---|---|
ShowSlice | store/show.ts | The show tree, dirty tracking, file path, and all structural edits |
ModeSlice | store/mode.ts | The active mode ("edit" or "perform"). |
PerformSlice | store/perform.ts | Highlight/active indices and navigation for perform mode. |
ImportSlice | store/import.ts | PDF 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.