How Meetily's Sidebar Context Pattern Works for State Management
TLDR: Meetily uses a React Context pattern through a SidebarProvider component that centralizes UI-wide state—including meeting selection, collapse status, recording controls, and transcript search—making it accessible to any descendant component via the useSidebar() hook without prop drilling.
Meetily is a desktop meeting application built with React and Tauri that requires complex UI state coordination between the sidebar navigation and main content areas. The application solves this challenge using a dedicated Sidebar Context pattern that encapsulates all sidebar-related state and business logic in a single provider component. This architecture ensures consistent state management across the frontend while maintaining clean separation between UI components and backend communication.
Core Architecture of the Sidebar Context
Context Definition and Type Safety
The pattern begins in frontend/src/components/Sidebar/SidebarProvider.tsx with createContext<SidebarContextType | null>(null), establishing a strongly-typed contract for all shared state. This approach ensures TypeScript enforcement throughout the component tree while providing a null fallback for safety checks.
Provider Component Structure
The SidebarProvider wraps the application and initializes state using useState for values like serverAddress, transcriptServerAddress, meetings, currentMeeting, and isCollapsed. It memoizes complex interactions through useCallback and manages side effects via useEffect, ensuring that derived state like sidebarItems updates efficiently when dependencies change.
State Initialization and Side Effects
Server Configuration Setup
When the provider mounts, it initializes serverAddress and transcriptServerAddress state variables. These URLs configure all Tauri-invoked backend calls, ensuring the sidebar communicates with the correct Rust backend endpoints for meeting data and transcript processing.
Meeting Data Synchronization
The provider fetches the meeting list using the Tauri command api_get_meetings whenever serverAddress changes. The results populate both the meetings state and drive the sidebarItems tree structure displayed in the navigation panel, ensuring the UI remains synchronized with the backend data source.
Routing Integration
The context monitors the Next.js pathname to maintain UI consistency. When the pathname equals /, the provider resets currentMeeting to the "+ New Call" placeholder and rebuilds sidebarItems accordingly, creating a seamless navigation experience between the home view and specific meeting details.
Advanced State Management Mechanisms
Recording State Coordination
Rather than duplicating recording state, the provider consumes useRecordingState() from frontend/src/contexts/RecordingStateContext.tsx. This ensures a single source of truth for the isRecording flag. The handleRecordingToggle() function dispatches custom events or navigates to the home page to synchronize UI and backend states, while frontend/src/lib/analytics.ts handles telemetry for these actions.
Summary Polling with Cleanup
For asynchronous summary generation, the provider implements startSummaryPolling() and stopSummaryPolling() methods. These store interval IDs in a Map<string, NodeJS.Timeout>, enabling per-meeting polling control and proper cleanup on component unmount to prevent memory leaks from orphaned timers.
Transcript Search Implementation
The searchTranscripts() function exposes backend search capabilities to UI components, managing isSearching and searchResults states during the operation. This allows any component to initiate searches without implementing duplicate fetch logic.
Consuming the Sidebar Context
Components access this state through the useSidebar() hook, which safely consumes the context and throws an error if called outside the provider boundary.
// Consume the context in a sidebar component
import { useSidebar } from '@/components/Sidebar/SidebarProvider';
function MeetingList() {
const {
meetings,
currentMeeting,
setCurrentMeeting,
toggleCollapse,
isCollapsed,
handleRecordingToggle,
} = useSidebar();
return (
<div>
<button onClick={toggleCollapse}>
{isCollapsed ? 'Expand' : 'Collapse'}
</button>
<ul>
{meetings.map(m => (
<li
key={m.id}
style={{
fontWeight: currentMeeting?.id === m.id ? 'bold' : 'normal',
}}
onClick={() => setCurrentMeeting(m)}
>
{m.title}
</li>
))}
</ul>
<button onClick={handleRecordingToggle}>Start / Stop</button>
</div>
);
}
// Trigger a transcript search from anywhere in the app
import { useSidebar } from '@/components/Sidebar/SidebarProvider';
function SearchBox() {
const { searchTranscripts, isSearching, searchResults } = useSidebar();
const [query, setQuery] = useState('');
const onSubmit = async (e) => {
e.preventDefault();
await searchTranscripts(query);
};
return (
<form onSubmit={onSubmit}>
<input value={query} onChange={e => setQuery(e.target.value)} />
<button type="submit" disabled={isSearching}>Search</button>
{isSearching && <p>Searching…</p>}
<ul>
{searchResults.map(r => (
<li key={r.id}>
<strong>{r.title}</strong> – {r.matchContext}
</li>
))}
</ul>
</form>
);
}
// Start and stop summary polling for a specific meeting
import { useSidebar } from '@/components/Sidebar/SidebarProvider';
function SummaryButton({ meetingId }: { meetingId: string }) {
const { startSummaryPolling, stopSummaryPolling } = useSidebar();
const start = () => {
startSummaryPolling(meetingId, 'process-123', (result) => {
console.log('Summary update:', result);
});
};
const stop = () => stopSummaryPolling(meetingId);
return (
<>
<button onClick={start}>Start Summary</button>
<button onClick={stop}>Stop Summary</button>
</>
);
}
Key Files and Responsibilities
| File | Role |
|---|---|
frontend/src/components/Sidebar/SidebarProvider.tsx |
Defines the SidebarContext, provider component, state hooks, backend invocations, and polling logic. |
frontend/src/contexts/RecordingStateContext.tsx |
Supplies the global isRecording flag used by the sidebar for toggle logic. |
frontend/src/lib/analytics.ts |
Handles telemetry for sidebar actions (e.g., start/stop recording). |
frontend/src/app/page.tsx |
Consumes the sidebar context to display meeting details and trigger recordings. |
Summary
- The Sidebar Context pattern centralizes all sidebar-related state in
frontend/src/components/Sidebar/SidebarProvider.tsx, eliminating prop drilling across the component tree. - State initialization occurs through
useStateanduseEffect, fetching meetings via Tauri'sapi_get_meetingscommand whenever server configuration changes. - Cross-context integration with
useRecordingState()ensures consistent recording status across the application without state duplication. - Memory management is handled through a
Map<string, NodeJS.Timeout>for polling intervals with explicit cleanup viastopSummaryPolling()and unmount effects. - Component access is standardized through the
useSidebar()hook, which provides type-safe access to state and actions while enforcing provider boundary constraints.
Frequently Asked Questions
What is the Sidebar Context pattern in Meetily?
Meetily's Sidebar Context pattern is a React Context implementation that encapsulates all sidebar-related state and logic within a SidebarProvider component. It provides a centralized store for meeting data, UI collapse states, recording controls, and search functionality, accessible via the useSidebar() hook throughout the component tree.
How does the sidebar handle recording state without duplication?
The sidebar delegates recording state to a separate RecordingStateContext by calling useRecordingState() from frontend/src/contexts/RecordingStateContext.tsx. This prevents state duplication and ensures the isRecording flag remains synchronized across the entire application, with the sidebar's handleRecordingToggle() function coordinating UI updates and backend dispatch.
What mechanism prevents memory leaks during summary polling?
The provider maintains a Map<string, NodeJS.Timeout> to track active polling intervals per meeting. The stopSummaryPolling() method clears specific intervals from this map, while the provider's cleanup logic ensures all intervals are terminated when the component unmounts, preventing memory leaks from orphaned timers.
How do components trigger a refresh of the meeting list?
Any component can call refetchMeetings() (exposed as fetchMeetings in the context value) to trigger a fresh fetch from the Rust backend via the api_get_meetings Tauri command. This function is memoized with useCallback to maintain stable references across renders, allowing components like frontend/src/app/page.tsx to refresh data on demand.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →