Good UX requires clear feedback about data loading status.
The Three States
jsxfunction DataDisplay() { const [data, setData] = useState(null); const [status, setStatus] = useState('idle'); // idle | loading | success | error const [error, setError] = useState(null); // Derived state const isIdle = status === 'idle'; const isLoading = status === 'loading'; const isError = status === 'error'; const isSuccess = status === 'success'; if (isIdle) return <button onClick={fetchData}>Load Data</button>; if (isLoading) return <Spinner />; if (isError) return <ErrorMessage error={error} onRetry={fetchData} />; return <DataView data={data} />; }
Loading Indicators
Spinner Component
jsxfunction Spinner({ size = 'medium' }) { return ( <div className={`spinner spinner-${size}`} role="status" aria-label="Loading" > <span className="sr-only">Loading...</span> </div> ); }
Skeleton Loading
Show placeholder content that mimics the final layout:
jsxfunction UserCardSkeleton() { return ( <div className="user-card"> <div className="skeleton skeleton-avatar" /> <div className="skeleton skeleton-text" /> <div className="skeleton skeleton-text short" /> </div> ); } function UserList() { const { data, isLoading } = useFetchUsers(); if (isLoading) { return ( <div> {[1, 2, 3].map(i => <UserCardSkeleton key={i} />)} </div> ); } return data.map(user => <UserCard key={user.id} user={user} />); }
Error Handling UI
jsxfunction ErrorMessage({ error, onRetry }) { return ( <div className="error-container" role="alert"> <h3>Something went wrong</h3> <p>{error.message}</p> {onRetry && ( <button onClick={onRetry}> Try Again </button> )} </div> ); }
Empty States
Don't forget the "no data" case:
jsxfunction SearchResults({ query, results }) { if (!query) { return <p>Enter a search term to begin</p>; } if (results.length === 0) { return ( <div className="empty-state"> <img src="/no-results.svg" alt="" /> <p>No results found for "{query}"</p> <p>Try adjusting your search terms</p> </div> ); } return ( <ul> {results.map(result => ( <li key={result.id}>{result.title}</li> ))} </ul> ); }
Progressive Loading
Show partial data as it becomes available:
jsxfunction Dashboard() { const { user, isLoading: userLoading } = useUser(); const { stats, isLoading: statsLoading } = useStats(); const { activity, isLoading: activityLoading } = useActivity(); return ( <div> {userLoading ? <HeaderSkeleton /> : <Header user={user} />} {statsLoading ? <StatsSkeleton /> : <StatsPanel stats={stats} />} {activityLoading ? <ActivitySkeleton /> : <ActivityFeed activity={activity} />} </div> ); }