Skip to main content
🤖AI-generated documentation curatedAI Generated
This page was drafted by an AI assistant and may contain inaccuracies.
About content generation types
🤖
AI GeneratedPage drafted entirely by AI from codebase or prompt instructions.
(e.g., docs generated from codebase analysis)
← this page
✋→🤖
AI TransformattedHuman provided raw material; AI restructured it into a different format.
(e.g., livestream → blog post, meeting notes → docs)
Human GeneratedPage written entirely by a human author.
(e.g., hand-written tutorial)
More info about content generation types ↗

Frontend State Management

State management uses Redux Toolkit with 12 active slice reducers (plus a 13th theme slice that exists on disk but is not registered in the store — it manages its own localStorage directly). Two custom listener middlewares provide selective localStorage persistence. Rapidly-changing data (camera frames, keypoints) bypasses Redux entirely and lives in refs inside ServerContextProvider.

Redux Store Architecture

The store is configured in src/store/store.ts:

export const store = configureStore({
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(cameraConfigListenerMiddleware.middleware)
.concat(persistenceListenerMiddleware.middleware),
reducer: {
cameras: cameraSlice.reducer,
recording: recordingSlice.reducer,
videos: videosSlice.reducer,
realtime: realtimeSlice.reducer,
calibration: calibrationSlice.reducer,
mocap: mocapSlice.reducer,
locale: localeSlice.reducer,
pipelines: pipelinesSlice.reducer,
blender: blenderSlice.reducer,
recordingStatus: recordingStatusSlice.reducer,
activeRecording: activeRecordingSlice.reducer,
playbackData: playbackDataReducer,
},
});

Typed hooks are re-exported from src/store/hooks.ts:

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Always use these typed versions — never the raw useDispatch / useSelector from react-redux. The types catch mistakes at compile time.

Slice Reference

Each slice owns one domain of state. Here's what each one holds and why it exists.

cameras

State: List of detected cameras, each with actualConfig (what the hardware reports), desiredConfig (what the user wants), connectionStatus, selected, realtimeEnabled, hasConfigMismatch. Plus isPaused, isLoading, autoApply, error.

Why it exists: Camera configuration is the foundation of the app. This slice tracks what cameras are available, whether they're connected, and whether their actual settings match what the user requested.

recording

State: isRecording, recordingDirectory, recordingName, startedAt, duration, config (naming convention, delay, microphone, type preset, auto-process), computed path, completionData, pendingOperation, countdown.

Why it exists: Video recording is a stateful operation with a lifecycle (idle → countdown → recording → complete). This slice manages that lifecycle and the naming/directory configuration.

videos

State: folder, files[], selectedFile, playbackState, isLoading, error.

Why it exists: Legacy video file management. In practice, playback now uses the playbackData slice and PlaybackContext instead. This slice is largely vestigial.

realtime

State: pipelineConfig (camera node config, aggregator config), cameraGroupId, pipelineId, isConnected, isLoading, error.

Why it exists: The realtime pipeline streams CharUco tracking and skeleton data over WebSocket. This slice tracks whether the realtime pipeline is connected and what its configuration is.

calibration

State: config (charuco board squares/rows/length, solver method, min shared views), isRecording, recordingProgress, directoryInfo, loadedCalibration, isLoading, error, dismissedCalibrationPath.

Why it exists: Calibration is a multi-step workflow: configure the CharUco board → start calibration recording → stop recording → run the calibration solver. This slice manages config and tracks progress through that workflow.

mocap

State: config (MediaPipe detector settings: model complexity, confidence thresholds, segmentation, face refinements; skeleton filter settings: One Euro Filter, FABRIK, bone length estimation, point gating, prediction), isRecording, recordingProgress, processingProgress, processingPhase, directoryInfo, calibrationTomlPath, isLoading, error.

Why it exists: Mocap processing is the core workflow. This slice manages the MediaPipe pose detection config, the skeleton filter config, and tracks progress through recording and post-processing.

locale

State: Current language code, previous language code, showTranslationIndicator.

Why it exists: Manages i18n language state. The actual translation strings live in i18next, not Redux — this slice just tracks which language is active for UI indicators and persistence.

pipelines

State: activePipelines (Record of PipelineProgress keyed by pipeline ID), dismissedBasePipelineIds, showCompleted, filterText, snackbarVisible.

Why it exists: Pipeline progress messages arrive over WebSocket and need to be tracked across the UI (progress bar, snackbar notifications, the pipeline progress panel). This slice aggregates progress from both calibration and mocap pipelines.

blender

State: blenderExePath, detectedBlenderExePath, exportToBlenderEnabled, autoOpenBlendFile, isExporting, isDetecting, isOpening, lastBlendFilePath, error.

Why it exists: Blender integration (detect, export .blend, open in Blender) is a cross-cutting feature that multiple components interact with. This slice tracks Blender's availability and export state.

recordingStatus

State: byRecordingId (Record of per-recording status entries), recordingsList[], recordingsIsLoading, recordingsFetchedAt.

Why it exists: After a recording completes, the backend processes it through stages (synchronized videos, annotated videos, calibration, blend file). This slice tracks which stages are complete for each recording, fetched via REST.

activeRecording

State: baseDirectory, recordingName, origin ('pending-capture' | 'just-captured' | 'browsed' | 'auto-latest'), layoutPreset.

Why it exists: The concept of a "currently active recording" is shared across many components — the recording info panel, the processing panel, the playback browser. This slice provides that shared identity.

playbackData

State: byRecordingId cache of PlaybackBundle (fps, totalFrames, duration, video sources, timestamps, calibration data, tracker schema, status summary).

Why it exists: Loading playback data from the backend is expensive (multiple REST calls). This slice caches loaded bundles so switching between recordings is fast.

Slice File Convention

The target file structure for slices is:

slices/<domain>/
├── <domain>-slice.ts State definition, reducers, actions
├── <domain>-thunks.ts Async thunks (API calls)
├── <domain>-types.ts TypeScript types and Zod schemas
├── <domain>-selectors.ts Memoized selectors (createSelector)
└── index.ts Barrel re-exports

In practice, slices vary in how closely they follow this convention. Slices like cameras, videos, and realtime follow it fully, while others inline types, selectors, or thunks into -slice.ts when the slice is simple enough. The convention is aspirational — use the full structure when a slice has enough complexity to warrant it.

Selectors often cross-reference multiple slices. For example, selectCanStartCalibrationRecording composes state from calibration and activeRecording. The convention is to place a selector in the slice that "owns" the question being asked.

Middleware

persistenceListenerMiddleware (300ms debounce)

Automatically persists selected slice state to localStorage. Runs after every Redux action, debounced at 300ms to avoid thrashing.

What gets persisted:

  • activeRecording (name, baseDirectory, layoutPreset)
  • recording.config
  • recording.directory
  • calibration.config
  • mocap.config
  • blender.settings (exePath, exportEnabled, autoOpen)

What doesn't: Camera state, pipeline progress, recording status, playback data, UI state. These are either ephemeral or can be refetched.

LocalStorage keys use a freemocap: prefix to avoid collisions.

cameraConfigListenerMiddleware (350ms debounce)

Watches for changes to camera desiredConfig. When autoApply is true and a config changes, it automatically dispatches camerasConnectOrUpdate to push the new config to the backend. Debounced at 350ms to batch rapid changes (e.g., dragging a slider).

Redux vs Context vs Refs

MechanismWhat uses itTriggers re-render?Persisted?
ReduxConfig, UI state, recording identityYesSelectively (localStorage)
ServerContextWebSocket state, frame/keypoint streams, overlay visibilityNo (uses refs internally)No
PlaybackContextLoaded videos, frame timestamps, source selectionYesNo
AutoUpdateContextElectron auto-updater stateYesNo
ViewportStateContextThree.js scene stateYesNo
useRef (in ServerContextProvider)Frame payloads, keypoints, rigid bodies, overlay dataNoNo

The rule: if data changes at display-refresh rate (60fps), it goes in a ref. If it needs to survive navigation, it goes in Redux. If it's shared across a specific subtree, it goes in Context.