How Meetily's SidebarContext Synchronizes React State with Rust Recording and Transcript State

Meetily uses a two-tier context architecture where RecordingStateContext mirrors Rust's recording lifecycle through Tauri events and polling, while SidebarProvider consumes this state to drive UI actions.

Meetily is an open-source meeting recorder built with a Rust backend and React frontend. Keeping the sidebar UI in sync with the native recording state requires bridging the async gap between Tauri's command layer and React's component lifecycle. The synchronization relies on RecordingStateContext acting as the single source of truth for recording flags, while SidebarProvider translates these flags into navigation and toggle behaviors.

The Architecture: From Rust Backend to React UI

The state flow follows a strict unidirectional pattern: Rust emits events → recordingService captures them → RecordingStateContext updates React state → SidebarProvider renders the correct UI.

RecordingStateContext as the Source of Truth

Located in frontend/src/contexts/RecordingStateContext.tsx, this global context maintains the canonical recording state. It exposes derived booleans like isRecording, isPaused, isStopping, and isSaving that the entire application consumes.

The context initializes by calling recordingService.getRecordingState() (lines 89-99) to hydrate React state with the current Rust backend status. This prevents the "page-refresh desync" bug where React loses track of an active recording after reload.

SidebarProvider as the UI Consumer

The SidebarProvider component in frontend/src/components/Sidebar/SidebarProvider.tsx imports useRecordingState (line 7) to access these flags. It uses isRecording to determine whether to show the "Start Recording" button or enable the "Stop" controls.

The handleRecordingToggle function (lines 45-64) reads isRecording before deciding whether to dispatch a custom DOM event (start-recording-from-sidebar) or navigate to the home page with an autoStartRecording flag in sessionStorage.

Initial State Hydration and Event-Driven Updates

Meetily employs three synchronization strategies to ensure the React sidebar never drifts from the Rust backend.

Mount-Time Synchronization

When RecordingStateContext mounts, it immediately executes syncWithBackend() to query the current Rust state:

// RecordingStateContext.tsx
useEffect(() => {
  // Pull current state from Rust on mount (lines 23, 89-99)
  syncWithBackend();
}, []);

This syncWithBackend function invokes the Tauri command get_recording_state, returning the active status, duration, and pause flags to initialize the React context correctly.

Real-Time Event Listeners

After hydration, the context registers listeners for the four lifecycle events: recording-started, recording-stopped, recording-paused, and recording-resumed (lines 40-80):

// RecordingStateContext.tsx
const unlistenStarted = await recordingService.onRecordingStarted(() => {
  setState(prev => ({ 
    ...prev, 
    isRecording: true, 
    status: RecordingStatus.RECORDING 
  }));
  startPolling();
});

These callbacks update the internal state object and trigger the derived status helper, ensuring any state change initiated from the Rust side (or another UI component) immediately reflects in the sidebar.

Polling for Active Recording State

While a recording is active, RecordingStateContext runs a 500ms polling loop to capture duration updates and pause toggles that might occur outside the discrete event stream:

// RecordingStateContext.tsx (lines 15-17)
useEffect(() => {
  pollingIntervalRef.current = setInterval(syncWithBackend, 500);
  return () => clearInterval(pollingIntervalRef.current);
}, [state.isRecording]);

The syncWithBackend method queries Rust for the latest durationSeconds and isPaused values. This polling automatically stops when the recording ends, preventing unnecessary overhead during idle states.

Integrating Transcript State

Transcript synchronization follows the same pattern through TranscriptContext (located in frontend/src/contexts/TranscriptContext.tsx). It subscribes to recordingService events like transcript-update and api_search_transcripts.

The sidebar consumes transcript capabilities via the searchTranscripts method (lines 66-84 of SidebarProvider.tsx), which only functions when the recording state allows. This coupling ensures transcript search is disabled until the Rust backend has processed audio segments.

The Tauri Bridge: recordingService

All communication flows through frontend/src/services/recordingService.ts, a thin wrapper around Tauri's invoke and listen APIs:

// recordingService.ts
async getRecordingState(): Promise<RecordingState> {
  return invoke<RecordingState>('get_recording_state');
}

async onRecordingStarted(cb: () => void): Promise<UnlistenFn> {
  return listen('recording-started', cb);
}

async startRecording(): Promise<void> {
  return invoke('start_recording');
}

This service singleton prevents multiple components from creating redundant Tauri listeners. It marshals complex Rust structs into TypeScript interfaces that RecordingStateContext can consume directly.

Summary

  • Initial sync: RecordingStateContext calls recordingService.getRecordingState() on mount to prevent desync after refresh.
  • Event-driven updates: Listeners for recording-started, stopped, paused, and resumed events push Rust state changes into React.
  • Active polling: A 500ms interval updates duration and pause flags while recording is active.
  • Sidebar consumption: SidebarProvider reads the isRecording flag to toggle between start/stop UI modes and handle navigation logic.
  • Transcript parity: TranscriptContext subscribes to the same event source, keeping search availability coupled to recording status.

Frequently Asked Questions

How does Meetily prevent the sidebar from showing the wrong recording state after a page refresh?

The RecordingStateContext executes syncWithBackend() immediately upon mounting (lines 89-99 of RecordingStateContext.tsx). This invokes the Rust command get_recording_state to hydrate React state with the actual backend status, fixing the "page-refresh desync" bug where the UI incorrectly assumed no recording was active.

What is the polling interval used for in RecordingStateContext?

While recording is active, the context runs setInterval(syncWithBackend, 500) (lines 15-17) to continuously fetch duration updates and pause status from Rust. Discrete events handle state transitions, but polling captures incremental duration changes that occur between events.

How does the sidebar trigger recording without directly calling Rust?

SidebarProvider uses the handleRecordingToggle function (lines 45-64) which checks the isRecording flag from context. If false, it either emits a custom start-recording-from-sidebar DOM event or sets sessionStorage flags and navigates to the home page. The actual Tauri invocation happens in the Home component, keeping the sidebar decoupled from direct backend calls.

Can transcript search work while recording is paused?

Transcript search availability depends on TranscriptContext, which listens to the same recordingService events as RecordingStateContext. While the analysis shows transcript updates occur during transcript-update events, the sidebar's searchTranscripts functionality (lines 66-84) is typically gated by the recording state, ensuring search operations align with the Rust backend's current processing capabilities.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →