Comprehensive guide to the React component architecture, custom hooks, and patterns used in the Nova Voice frontend application.
The frontend follows a modern React architecture with custom hooks for state management, modular components, and clean separation of concerns.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ App (page.tsx)│────│ Custom Hooks │────│ Components │
│ │ │ │ │ │
│ • State orchestration│ • useConnection │ • ModeButtons │
│ • Component composition│ • useAudioRecording│ • LangSelectors │
│ • Layout management │ • useWindowSizing │ • StatusIndicator │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Services │ │ UI Library │
│ │ │ │
│ • Gateway API │ │ • shadcn/ui │
│ • Audio devices │ │ • Tailwind CSS │
│ • Config │ │ • Lucide icons │
└─────────────────┘ └─────────────────┘
- Custom hooks encapsulate complex state logic
- Separation of concerns between UI and business logic
- Reusable logic across components
- Testable units for state management
- Small, focused components with single responsibilities
- Props-driven interfaces for flexibility
- Composition over inheritance patterns
- TypeScript for type safety
- Thin service wrappers around external APIs
- Consistent error handling across services
- Environment-based configuration
- Easy mocking for testing
Purpose: Manages WebSocket connection to backend gateway service.
Location: hooks/useConnection.ts
Features:
- Connection state tracking (
connected,connecting) - Mode switching (
typing|subtitle) - Automatic reconnection logic
- Error handling and status reporting
API:
const { connected, connecting, mode, connect, disconnect, setMode } = useConnection()Usage:
// Connect to gateway
await connect()
// Switch modes
await setMode('typing') // or 'subtitle'
// Check status
if (connected && mode === 'typing') {
// Enable voice typing features
}State Management:
interface ConnectionState {
connected: boolean
connecting: boolean
mode: 'typing' | 'subtitle'
}Purpose: Wraps audio recording functionality with device management.
Location: hooks/useAudioRecording.ts
Features:
- Audio level monitoring
- Device selection and switching
- Recording state management
- Real-time audio data processing
API:
const {
isRecording,
audioLevel,
selectedDevice,
setSelectedDevice,
startRecording,
stopRecording
} = useAudioRecording()Usage:
// Start recording
await startRecording()
// Monitor audio levels
console.log('Audio level:', audioLevel)
// Change device
setSelectedDevice('device-id')Purpose: Handles global keyboard shortcuts from Electron.
Location: hooks/useShortcuts.ts
Features:
- Shortcut registration and handling
- Action dispatching to components
- Electron IPC integration
- Cross-platform shortcut support
API:
// Hook sets up listeners automatically
// Actions are dispatched via callback propsIntegration:
// In component that needs shortcuts
function handleVoiceTyping() {
// Toggle voice typing mode
}
function handleLiveSubtitles() {
// Toggle live subtitles mode
}
// Hook listens for IPC messages and calls appropriate handlersPurpose: Manages dynamic Electron window resizing based on UI state.
Location: hooks/useWindowSizing.ts
Features:
- ResizeObserver integration
- Dropdown height calculations
- Smooth window transitions
- Panel state management
API:
const {
rootRef,
toolbarRef,
settingsRef,
showSettings,
toggleSettings
} = useWindowSizing(openDropdown, pendingDropdown)Usage:
// Attach refs to DOM elements
<div ref={rootRef}>
<div ref={toolbarRef}>
{/* Toolbar content */}
</div>
{showSettings && (
<div ref={settingsRef}>
{/* Settings panel */}
</div>
)}
</div>Location: components/control/ModeButtons.tsx
Purpose: Voice typing and live subtitle mode toggles.
Props:
interface ModeButtonsProps {
voiceTypingActive: boolean
liveSubtitleActive: boolean
onToggleVoiceTyping: () => void
onToggleLiveSubtitle: () => void
}Features:
- Visual state indicators
- Keyboard shortcut hints
- Disabled states during transitions
Location: components/control/LangSelectors.tsx
Purpose: Source and target language selection.
Props:
interface LangSelectorsProps {
sourceLanguage: string
targetLanguage: string
onSourceLanguageChange: (lang: string) => void
onTargetLanguageChange: (lang: string) => void
openDropdown: 'source' | 'target' | 'audio' | null
setOpenDropdown: (dropdown: 'source' | 'target' | 'audio' | null) => void
setPendingDropdown: (dropdown: 'source' | 'target' | 'audio' | null) => void
}Features:
- Controlled dropdown state
- Dynamic window resizing
- Language validation
Location: components/control/AudioSource.tsx
Purpose: Audio input device selection.
Props:
interface AudioSourceProps {
selectedAudioDevice: string
onAudioDeviceChange: (device: string) => void
openDropdown: 'source' | 'target' | 'audio' | null
setOpenDropdown: (dropdown: 'source' | 'target' | 'audio' | null) => void
setPendingDropdown: (dropdown: 'source' | 'target' | 'audio' | null) => void
}Features:
- Device enumeration integration
- System audio capture support
- Permission handling
Location: components/control/StatusIndicator.tsx
Purpose: Real-time connection and recording status display.
Props:
interface StatusIndicatorProps {
running: boolean
connected: boolean
listening: boolean
}States:
- Inactive: Not connected, not recording
- Connected: Connected to gateway, not recording
- Listening: Connected and actively recording
Location: components/settings/SettingsPanel.tsx
Purpose: Keybind configuration and device settings.
Features:
- Audio device selection
- Keyboard shortcut inputs
- Settings persistence
Location: components/ui/
Components Used:
Button- Consistent button stylingSelect- Dropdown components- Custom styling with Tailwind CSS
Components compose multiple hooks for complex functionality:
function VoiceTranscriberApp() {
// Connection management
const { connected, connecting, mode, connect, disconnect, setMode } = useConnection()
// Audio processing
const {
isRecording,
audioLevel,
selectedDevice,
setSelectedDevice,
startRecording,
stopRecording
} = useAudioRecording()
// Window management
const {
rootRef,
toolbarRef,
settingsRef,
showSettings,
toggleSettings,
} = useWindowSizing(openDropdown, pendingDropdown)
// UI state
const [sourceLanguage, setSourceLanguage] = useState("en")
const [targetLanguage, setTargetLanguage] = useState("vi")
// ... component logic
}Thin wrappers around external APIs:
// lib/gateway.ts - WebSocket API wrapper
export const gateway = {
connect: () => window.electronAPI.connectGateway(),
disconnect: () => window.electronAPI.disconnectGateway(),
sendAudio: (u8: Uint8Array, sr: number) => window.electronAPI.sendAudioData(u8, sr),
setMode: (mode: 'typing'|'subtitle') => window.electronAPI.setMode(mode),
updateLanguages: (src: string, dst: string) => window.electronAPI.updateLanguages(src, dst),
startOver: () => window.electronAPI.sendStartOver(),
}User Action → Component Event → Hook Update → State Change → Re-render
↓
Service Call → External API → Response → State Update → UI Update
WebSocket Message → Electron IPC → Hook Listener → State Update → Component Re-render
User Speech → Microphone → Web Audio API → Audio Recorder → Binary Encoding → WebSocket → Gateway
// Test custom hooks in isolation
import { renderHook, act } from '@testing-library/react'
import { useConnection } from '@/hooks/useConnection'
test('connection hook works', () => {
const { result } = renderHook(() => useConnection())
act(() => {
result.current.connect()
})
expect(result.current.connecting).toBe(true)
})// Test components with mocked hooks
import { render, screen } from '@testing-library/react'
import { ModeButtons } from '@/components/control/ModeButtons'
const mockProps = {
voiceTypingActive: false,
liveSubtitleActive: false,
onToggleVoiceTyping: jest.fn(),
onToggleLiveSubtitle: jest.fn(),
}
test('mode buttons render correctly', () => {
render(<ModeButtons {...mockProps} />)
expect(screen.getByText('Voice Typing')).toBeInTheDocument()
})// Memoize expensive computations
const processedData = useMemo(() => {
return expensiveCalculation(data)
}, [data])// Prevent unnecessary re-renders
const MemoizedComponent = memo(function Component({ prop }) {
return <div>{prop}</div>
})// Load components on demand
const SettingsPanel = lazy(() => import('@/components/settings/SettingsPanel'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SettingsPanel />
</Suspense>
)
}When creating new components:
- Single Responsibility: Does one thing well
- TypeScript Types: Proper type definitions
- Props Interface: Clear prop contracts
- Default Props: Sensible defaults where appropriate
- Accessibility: ARIA labels, keyboard navigation
- Error Boundaries: Graceful error handling
- Loading States: Handle async operations
- Responsive Design: Works on different screen sizes
When creating custom hooks:
- Clear Purpose: Single, well-defined responsibility
- Consistent API: Similar patterns to existing hooks
- TypeScript Support: Full type safety
- Error Handling: Proper error states and recovery
- Cleanup: Proper effect cleanup
- Testing: Unit tests for hook logic
- Documentation: JSDoc comments for complex logic
- DEVELOPMENT_SETUP.md - Development workflow
- VOICE_TYPING_ENGINE.md - Typing simulation algorithm
- ELECTRON_INTEGRATION.md - Main/renderer processes
- WEBSOCKET_CLIENT.md - Backend communication
- AUDIO_MANAGEMENT.md - Audio handling
The component architecture emphasizes modularity, reusability, and maintainability through custom hooks and clean component composition.