# State Machines — The Flow of Data > *"All happy families are alike; each unhappy family is unhappy in its own way."* > — Tolstoy, adapted for component states. --- ## 🎯 Filosofi State Machine Setiap komponen data-driven di BETE memiliki **4 state fundamental**: ``` IDLE → LOADING → SUCCESS ↘ ERROR ↘ EMPTY (conditional, jika data.length === 0) ``` State machine memastikan **tidak ada kondisi yang terlewat** — setiap kemungkinan state visual memiliki representasi. --- ## 🎮 The Quad-State Pattern ```tsx type DataState = | { status: 'idle' } | { status: 'loading'; progress?: number } | { status: 'success'; data: T; timestamp: number } | { status: 'error'; error: Error; retryCount?: number } | { status: 'empty'; message?: string }; ``` ### Generic State Machine Hook ```tsx // shared/hooks/useDataState.ts function useDataState( fetcher: () => Promise, options?: { onSuccess?: (data: T) => void; onError?: (error: Error) => void; retry?: number; cacheKey?: string; } ): { state: DataState; execute: () => Promise; reset: () => void; retry: () => Promise; setData: (data: T) => void; } { const [state, setState] = useState>({ status: 'idle' }); const execute = useCallback(async () => { setState({ status: 'loading' }); try { const data = await fetcher(); if (Array.isArray(data) && data.length === 0) { setState({ status: 'empty', message: 'No data available' }); } else { setState({ status: 'success', data, timestamp: Date.now() }); options?.onSuccess?.(data); } } catch (error) { setState({ status: 'error', error: error as Error }); options?.onError?.(error as Error); } }, [fetcher]); return { state, execute, reset, retry: execute, setData }; } ``` ### Component Rendering ```tsx function DataPanel() { const { state, execute, retry } = useDataState(fetchMessages); useEffect(() => { execute(); }, []); switch (state.status) { case 'idle': case 'loading': return ; case 'error': return ( ); case 'empty': return ; case 'success': return ; } } ``` --- ## 🖼️ Visual Representations ### Loading State ```tsx interface LoadingSkeletonProps { variant?: 'card' | 'list' | 'detail' | 'table' | 'chart'; count?: number; // Jumlah skeleton items } /* Contoh variant 'card' */ function CardSkeleton() { return (
); } ``` ### Error State ```tsx interface ErrorStateProps { error: Error; onRetry: () => void; retryCount?: number; variant?: 'inline' | 'full-page' | 'toast'; } function ErrorState({ error, onRetry, retryCount }: ErrorStateProps) { const isRetryExhausted = (retryCount ?? 0) >= 3; return (

Something went wrong

{error.message}

{!isRetryExhausted ? ( ) : (

Still failing after multiple attempts. Please try again later.

)}
); } ``` ### Empty State ```tsx interface EmptyStateProps { icon?: ReactNode; title: string; description?: string; action?: { label: string; onClick: () => void }; variant?: 'mascot' | 'icon' | 'minimal'; } const EMPTY_STATES = { messages: { icon: MessageSquare, title: 'No messages yet', description: 'Messages will appear here once they are captured.' }, speakers: { icon: Mic, title: 'No active speakers', description: 'Quiet in here...' }, recordings: { icon: Radio, title: 'No recordings', description: 'Join a voice channel to start recording.' }, analytics: { icon: BarChart3, title: 'Not enough data', description: 'Analytics will populate as data accumulates.' }, users: { icon: Users, title: 'No users found', description: 'Try adjusting your filters.' }, }; ``` --- ## ♻️ State Transition Diagram ``` ┌──────────┐ │ IDLE │ └────┬─────┘ │ execute() ↓ ┌──────────┐ │ LOADING │◄────── retry() └────┬─────┘ │ ┌───────┴───────────┐ │ │ ↓ ↓ ┌──────────┐ ┌──────────┐ │ SUCCESS │ │ ERROR │ │ data: T │ │ err: E │ └────┬─────┘ └────┬─────┘ │ │ │ (data.length │ retry() │ === 0) │ ↓ │ ┌──────────┐ │ │ EMPTY │ │ │ msg: str │ │ └──────────┘ │ │ │ └──────┬───────────┘ │ reset() ↓ ┌──────────┐ │ IDLE │ └──────────┘ ``` --- ## 🔄 Retry Strategy ```typescript const RETRY_CONFIG = { maxAttempts: 3, baseDelay: 1000, // 1s maxDelay: 10000, // 10s backoff: 'exponential' as const, onRetry: (attempt: number, error: Error) => { logger.warn(`Retry attempt ${attempt}`, { error: error.message }); }, }; ``` ### Exponential Backoff ```typescript function calculateDelay(attempt: number): number { return Math.min( 1000 * Math.pow(2, attempt - 1), // 1s, 2s, 4s 10000 // cap at 10s ); } ``` --- ## 📦 Component State Map | Component | Loading | Error | Empty | Success | |-----------|---------|-------|-------|---------| | MessageFeed | Card skeletons | ErrorState + retry | Mascot "No messages" | Message list | | VoiceCards | Card skeletons | ErrorState | "No connected channels" | VoiceCard list | | ActiveSpeakers | Dot skeletons | Silent fallback | "No speakers" | Speaker list | | Analytics | Skeleton grid | ErrorState | "Not enough data" | Charts | | Recordings | List skeletons | ErrorState | "No recordings" | Recording list | | UserList | List skeletons | ErrorState + retry | "No users found" | User list | | DashboardStats | Stat skeletons | ErrorState | "No data available" | Stat grid | --- ## ⚠️ Anti-Patterns State ### ❌ Missing state handling ```tsx // ❌ JANGAN — hanya handle SUCCESS function Panel() { const { data, isLoading } = useQuery(...); if (isLoading) return ; return ; // ERROR? EMPTY? } ``` ### ❌ Loading state after error ```tsx // ❌ JANGAN — loading infinite loop setelah error function Panel() { const { data, isLoading } = useQuery(..., { retry: true }); if (isLoading) return ; // ERROR: retry=true + error = loading terus } ``` ### ❌ Empty state default terlalu generic ```tsx // ❌ JANGAN — tidak helpful
No data
// ✅ Kontekstual dengan action ``` --- ## 🔗 Referensi | Sumber | Konsep | |--------|--------| | [State Reducer Pattern](https://kentcdodds.com/blog/state-reducer-pattern) | Advanced state management | | [XState](https://stately.ai/docs/xstate) | Visual state machines | | [React useReducer](https://react.dev/reference/react/useReducer) | Built-in state management | --- *"Setiap state adalah babak dalam cerita data — dari sunyi hingga berbicara."* ❄️🩵