The Mental Model
The 4-Screen Rule.
Beginners get stuck building "Settings" pages and "Forgot Password" flows. Stop.
The Hook: Landing Page — Sell the promise. One headline, one CTA.
The Home: Dashboard — Show the state. What does the user have?
The Work: Action Page — The form, the creator, the editor.
The Payoff: Success State — Confetti, receipt, confirmation.
// The Mindset
To validate an idea, you only need the "Happy Path" — the exact sequence of clicks a user takes to solve their problem. Ignore edge cases.
The Screens
Landing Page
THE HOOK
One job: make them click the button. Hero headline, subtext, CTA.
Dashboard
THE HOME
Show what the user "has." List of items, stats, recent activity.
Action Page
THE WORK
The form. The editor. Where the user does the thing.
Success State
THE PAYOFF
Dopamine hit. Confirmation, receipt, celebration.
The Data Layer
Hardcode Everything.
Do not build a database. Do not build an API. Create a file with your dream data inside it. This is your "mock."
- SpeedIterate on UI without waiting for backend.
- FocusDesign the ideal data shape for your UI first.
- LaterReplace with real API calls when ready.
// src/lib/data.ts
export const listings = [
{
id: 1,
title: "Luxury Beach Villa",
price: 250,
image: "/villa.jpg",
rating: 4.9,
},
{
id: 2,
title: "Mountain Cabin",
price: 120,
// ...
},
];
export type Listing = typeof listings[0];
Import & Map
Import your fake data and map over it like it's a real API response.
import { listings } from "@/lib/data";
export default function Dashboard() {
return (
<div>
{listings.map((item) => (
<Card key={item.id} {...item} />
))}
</div>
);
}
Fake Mutations
For "create" or "delete" actions, use local state. It won't persist on refresh, but it'll feel real.
const [items, setItems] = useState(listings);
const addItem = (newItem) => {
setItems([...items, {
...newItem,
id: Date.now(), // fake ID
}]);
};
The State Machine
You don't need a router yet. Use a simple state variable to swap between views. It's fast and easy to debug.
The "If" Router
const [view, setView] = useState("landing");
return (
<>
{view === "landing" && <Landing />}
{view === "dashboard" && <Dashboard />}
{view === "action" && <ActionPage />}
{view === "success" && <Success />}
</>
);
Trigger Navigation
// In Landing component
<Button
onClick={() => setView("dashboard")}
>
Get Started
</Button>
// In ActionPage after form submit
const handleSubmit = () => {
addItem(formData);
setView("success");
};
Pro tip: Pass setView as a prop to child components, or use React Context if it gets messy.
Common Patterns
✗ Scope Creep
"I need user authentication"
No you don't. Use a fake "logged in" boolean for the demo.
"I need a database"
No you don't. Hardcode an array. It's faster to iterate.
"I need error handling"
For a demo? Just assume everything works. Handle errors later.
"I need a settings page"
Settings pages don't validate ideas. Build the happy path first.
✓ Demo Mindset
Fake the login
setLoggedIn(true) on button click. Done.
Use placeholder images
Unsplash URLs or placehold.co. Don't waste time on assets.
Hardcode happy data
5 perfect items. No edge cases. No empty states (yet).
Ship it ugly
A working demo beats a polished mockup. Get clicks first.
The Exercises
Week 02 Final Exam.
The Dictionary
Create Fake Data
- →Create src/lib/data.ts
- →Export an array of 5 items
- →Include: id, title, price, image
- →Export the type
The Flow
Wire Up Navigation
- →Create 4 component files
- →Add useState("landing")
- →Wire button clicks to setView
- →Test the full flow
The Demo
Ship It
- →Map data to Dashboard cards
- →Add a form to Action page
- →On submit, show Success
- →Record a 30-second demo video
📋 Quick Reference Cheatsheet
Data File
export const items = [...]State Router
{view === "x" && <X />}Navigate
onClick={() => setView("next")}