Objective
Your mission
Audit and fix data-driven components with loading states, error handling, empty states, optimistic updates, and query limits. Output: data_patterns_audit.md.
You need: A Supabase-connected app with data-driven components.
Time: 30 minutes.
Step 1: learn the three-state principle (5 min)
Every data-driven component has three possible states. Most AI-generated code only handles success.
The three-state principle
Every data-driven component has three possible states: loading, error, and success. Most AI-generated code only handles success — it assumes data arrives, is correct, and is non-empty. In production, loading and error states are more common than success states. Handle all three.
The pattern:
function HabitList() {
const [habits, setHabits] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
async function fetchHabits() {
const { data, error } = await supabase.from('habits').select('*')
if (error) {
setError(error.message)
} else {
setHabits(data)
}
setLoading(false)
}
fetchHabits()
}, [])
if (loading) return <LoadingSkeleton />
if (error) return <ErrorMessage message={error} />
if (habits.length === 0) return <EmptyState />
return habits.map(h => <HabitCard key={h.id} habit={h} />)
}
Step 2: audit each data component (10 min)
Open your app. For each component that fetches data, check:
Write in your file:
## Component Audit
### Component: [name]
- Loading state: [yes/no — does it show a skeleton/spinner while data loads?]
- Error state: [yes/no — does it show a user-friendly message if the query fails?]
- Empty state: [yes/no — does it show an empty state if there's no data?]
- Error check before using data: [yes/no — `if (error)` before using `data`?]
### Component: [name]
[repeat for each data component]
Step 3: fix missing states (10 min)
For each component missing a state, add it:
- Loading: Add
const [loading, setLoading] = useState(true), show skeleton/spinner, set false when data arrives - Error: Add
const [error, setError] = useState(null), check if (error) before using data, show user-friendly message - Empty: After data loads, check
if (habits.length === 0) and show empty state
Write in your file:
## Fixes Applied
1. [component]: Added [loading/error/empty] state
2. [component]: Added [loading/error/empty] state
[repeat for each fix]
Step 4: add optimistic updates (5 min)
Pick one interaction (e.g., toggle complete, delete item). Add optimistic updates:
async function toggleHabit(habitId, currentCompleted) {
// Optimistic: update UI immediately
setHabits(habits.map(h =>
h.id === habitId ? { ...h, completed: !currentCompleted } : h
))
// Send to database
const { error } = await supabase
.from('habits')
.update({ completed: !currentCompleted })
.eq('id', habitId)
// Revert if failed
if (error) {
setHabits(habits.map(h =>
h.id === habitId ? { ...h, completed: currentCompleted } : h
))
showError('Could not update habit. Try again.')
}
}
Write in your file:
## Optimistic Updates
Component: [name]
Interaction: [e.g., toggle complete]
UI updates immediately: [yes/no]
Reverts on error: [yes/no]
Step 5: add query limits (3 min)
Find any query that could return too many rows. Add a limit:
// Before — loads ALL habits
const { data } = await supabase.from('habits').select('*')
// After — loads first 50
const { data } = await supabase
.from('habits')
.select('*')
.order('created_at', { ascending: false })
.range(0, 49)
Write in your file:
## Query Limits
Query: [description] — Limit added: [yes/no — what limit?]
Step 6: test error handling (2 min)
Turn off your network (or stop the dev server). See how the app handles errors.
Write in your file:
## Error Test
Network off — app shows error messages: [yes/no]
No crashes from null data: [yes/no]
User-friendly messages shown: [yes/no]
Your habit list works on your fast home Wi-Fi but shows a blank screen on a train. Which pattern is missing?
Most AI-generated components only handle success. Every data component needs loading, error and empty states as well.