The Mental Model
Isolate the Variable.
When the speakers buzz, you don't rewrite the song. You check the cable. In dev, if your app breaks, is it the UI or the Data?
The Mock Phase: Build the UI using a static array. If it breaks here, it's a React bug.
The Swap Phase: Replace the array with a fetch call. If it breaks here, it's a Database bug.
// The Golden Rule
"Never debug UI logic and Data fetching logic at the same time."
The Studio
Sign the Contract.
Before writing fake data OR real code, define what the data looks like. This is your contract. If the Mock signs it, and the Database signs it, the Swap will be instant.
Why this matters:
- ✓Autocompletes your fake data for you.
- ✓Errors red if your DB returns the wrong shape.
- ✓Documentation for your future self.
// types/index.ts
export interface Track {
id: string;
title: string;
artist: string;
bpm: number;
is_active: boolean;
tags: string[];
}
// The component doesn't care source
function Player({ track }: { track: Track })
The Mock
A simple file. No async, no await, no loading states. Instant feedback.
import { Track } from "@/types";
export const mockTracks: Track[] = [
{
id: "1",
title: "Bassline",
artist: "System",
bpm: 128,
is_active: true,
tags: ["House"],
},
];
The Real
The component expects `Track[]`. It doesn't care if it came from memory or Supabase.
import { Track } from "@/types";
export async function getTracks(): Promise<Track[]> {
const { data } = await supabase
.from('tracks')
.select('*');
return data as Track[];
}
The Swap
Phase 1: Hardcoded
Import directly from your data file. No waiting.
// page.tsx
import { mockTracks } from "@/lib/data";
export default function Page() {
// Just pass the array
return <TrackList items={mockTracks} />
}
Phase 2: Fetched
Change the import to the API function. Add await.
// page.tsx
import { getTracks } from "@/lib/api";
export default async function Page() {
// Fetch the data
const tracks = await getTracks();
return <TrackList items={tracks} />
}
The State Machine
Mock data is instant. Real data takes time. When you swap, you need to handle the waiting.
Loading
if (loading) {
return <Skeleton />
}
Show a placeholder while fetching.
Error
if (error) {
return <ErrorMsg />
}
Show a message if the fetch fails.
Success
return (
<TrackList
items={data}
/>
)
Render the UI with real data.
Pro tip: With mocks, you skip loading/error states entirely. Add them only when you swap to real data.
Common Patterns
✗ Signal Distortion
"I'll fetch data from day one"
Now every bug could be UI OR backend. Good luck debugging.
"I don't need types"
Then your mock and real data will have different shapes. Swap will fail.
"I'll handle loading states later"
Your app will flash blank screens. Users will think it's broken.
"My mock data is just [1, 2, 3]"
Lazy mocks hide bugs. Use realistic data with edge cases.
✓ Clean Signal
Types first, always
Define the interface. Mock signs it. Real signs it. Swap is instant.
Realistic mock data
Include long titles, empty arrays, missing images. Find bugs early.
One swap at a time
Don't replace all mocks at once. Swap one endpoint, test, repeat.
Keep mocks around
Comment out, don't delete. Quick rollback when the API breaks.
The Exercises
Set the stage for the database.
The Contract
Define TypeScript
- →Create types/index.ts
- →Look at your Napkin Flow (Track 08)
- →Write interfaces for your data
- →Export them
The Mock
Static File
- →Create lib/mock-data.ts
- →Import your Interface
- →Fill it with perfect, ideal data
- →Include edge cases (long titles, etc)
The Prop Drill
Pass it Down
- →Import mock data in your Page
- →Pass it to your Components
- →Verify the UI looks good
- →Do not fetch yet. Just render.
📋 Quick Reference Cheatsheet
Type Definition
interface User { id: string }Mock Data
const users: User[] = [...]Supabase Typecast
data as User[]