Component Hierarchy
Core Principles
- Server Components fetch data - Server Components fetch data directly, Client Components receive data as props
- Atomic Design - Build from atoms → molecules → organisms → pages
- Hooks in Client Components - Custom hooks belong in Client Components (organisms), not Server Components
- Performance & SEO first - Always prioritize unless there's an edge case
- Never use
any- Under no circumstances
Atomic Design Hierarchy
txt1Page (src/app/) 2└── Organism (large component with business logic) 3 └── Molecule (group of atoms with simple logic) 4 └── Atom (single UI element, no logic)
Atoms
Single UI elements with no business logic. Receive props, render UI.
tsx1// src/ui/custom/Button/Button.tsx 2interface ButtonProps { 3 children: ReactNode; 4 onClick?: () => void; 5 variant?: "primary" | "secondary"; 6 disabled?: boolean; 7} 8 9export default function Button({ children, onClick, variant = "primary", disabled }: ButtonProps) { 10 return ( 11 <button onClick={onClick} disabled={disabled} className={`btn btn-${variant}`}> 12 {children} 13 </button> 14 ); 15}
tsx1// src/ui/custom/Input/Input.tsx 2interface InputProps { 3 value: string; 4 onChange: (value: string) => void; 5 placeholder?: string; 6 type?: "text" | "email" | "password"; 7} 8 9export default function Input({ value, onChange, placeholder, type = "text" }: InputProps) { 10 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { 11 onChange(e.target.value); 12 }; 13 14 return <input type={type} value={value} onChange={handleChange} placeholder={placeholder} />; 15}
Molecules
Group of atoms with simple interaction logic. No hooks, no data fetching.
tsx1// src/ui/custom/SearchInput/SearchInput.tsx 2import Input from "../Input"; 3import Button from "../Button"; 4 5interface SearchInputProps { 6 value: string; 7 onChange: (value: string) => void; 8 onSearch: () => void; 9 placeholder?: string; 10} 11 12export default function SearchInput({ value, onChange, onSearch, placeholder }: SearchInputProps) { 13 const handleKeyDown = (e: React.KeyboardEvent) => { 14 if (e.key === "Enter") { 15 onSearch(); 16 } 17 }; 18 19 return ( 20 <div className="flex gap-2" onKeyDown={handleKeyDown}> 21 <Input value={value} onChange={onChange} placeholder={placeholder} /> 22 <Button onClick={onSearch}>Search</Button> 23 </div> 24 ); 25}
tsx1// src/ui/custom/UserCard/UserCard.tsx 2import Avatar from "../Avatar"; 3import Badge from "../Badge"; 4 5interface UserCardProps { 6 name: string; 7 email: string; 8 avatarUrl?: string; 9 role: "admin" | "user"; 10} 11 12export default function UserCard({ name, email, avatarUrl, role }: UserCardProps) { 13 return ( 14 <div className="flex items-center gap-4 p-4 border rounded"> 15 <Avatar src={avatarUrl} alt={name} /> 16 <div> 17 <h3 className="font-bold">{name}</h3> 18 <p className="text-gray-500">{email}</p> 19 </div> 20 <Badge variant={role === "admin" ? "primary" : "secondary"}>{role}</Badge> 21 </div> 22 ); 23}
Organisms
Large components with business logic. Can use hooks, stores, and handle complex interactions.
tsx1// src/ui/custom/UserList/UserList.tsx 2import { useState } from "react"; 3import { useDebounce } from "@/hooks"; 4import { useUIStore } from "@/stores"; 5import SearchInput from "../SearchInput"; 6import UserCard from "../UserCard"; 7 8interface User { 9 id: string; 10 name: string; 11 email: string; 12 avatarUrl?: string; 13 role: "admin" | "user"; 14} 15 16interface UserListProps { 17 users: User[]; 18 onUserSelect: (user: User) => void; 19} 20 21export default function UserList({ users, onUserSelect }: UserListProps) { 22 const [searchTerm, setSearchTerm] = useState(""); 23 const debouncedSearch = useDebounce(searchTerm, 300); 24 const { openModal } = useUIStore(); 25 26 const filteredUsers = users.filter( 27 (user) => 28 user.name.toLowerCase().includes(debouncedSearch.toLowerCase()) || 29 user.email.toLowerCase().includes(debouncedSearch.toLowerCase()) 30 ); 31 32 const handleUserClick = (user: User) => { 33 onUserSelect(user); 34 openModal(); 35 }; 36 37 const handleSearch = () => { 38 // Optional: trigger immediate search 39 }; 40 41 return ( 42 <div className="space-y-4"> 43 <SearchInput 44 value={searchTerm} 45 onChange={setSearchTerm} 46 onSearch={handleSearch} 47 placeholder="Search users..." 48 /> 49 <div className="space-y-2"> 50 {filteredUsers.map((user) => ( 51 <div key={user.id} onClick={() => handleUserClick(user)} className="cursor-pointer"> 52 <UserCard {...user} /> 53 </div> 54 ))} 55 </div> 56 </div> 57 ); 58}
Pages (Server Components)
Server Components fetch data directly. Client Components receive data as props.
tsx1// src/app/users/page.tsx 2import { UserList } from "@/components"; 3 4async function getUsers() { 5 const res = await fetch("https://api.example.com/users", { 6 next: { revalidate: 60 }, 7 }); 8 return res.json(); 9} 10 11export default async function UsersPage() { 12 const users = await getUsers(); 13 14 return ( 15 <div> 16 <h1 className="text-2xl font-bold mb-4">Users</h1> 17 <UserList users={users} /> 18 </div> 19 ); 20}
Pages (Client Components)
When you need interactivity, use Client Components:
tsx1// src/app/users/page.tsx 2"use client"; 3 4import { UserList } from "@/components"; 5import { useUsers } from "@/hooks/users"; 6 7export default function UsersPage() { 8 const { data: users, isLoading } = useUsers(); 9 10 const handleUserSelect = (user: { id: string; name: string }) => { 11 console.log("Selected user:", user.id); 12 }; 13 14 if (isLoading) return <div>Loading...</div>; 15 16 return ( 17 <div> 18 <h1 className="text-2xl font-bold mb-4">Users</h1> 19 <UserList users={users} onUserSelect={handleUserSelect} /> 20 </div> 21 ); 22}
Data Flow
txt1Server Data Flow (Server Components): 2API → Server Component (fetch) → Organism → Molecule → Atom 3 (props) (props) (props) 4 5Client Data Flow (Client Components): 6API → React Query Hook → Client Component → Organism → Molecule → Atom 7 (props) (props) (props) (props) 8 9Client State Flow: 10User Action → Atom (onClick) → Molecule (handler) → Organism (hook/store) → UI Update
When to Create a New Component
| Scenario | Create New? | Level |
|---|---|---|
| Button with icon | No, add icon prop to Button | Atom |
| Search bar (input + button) | Yes | Molecule |
| Form with validation | Yes | Organism |
| Repeating UI pattern (3+ times) | Yes | Appropriate level |
| Single-use complex UI | Maybe, if >100 lines | Organism |
Hook Usage Rules
| Component Level | Can Use Hooks? | Examples |
|---|---|---|
| Atom | No | Button, Input, Badge |
| Molecule | Rarely (only useState for local UI) | SearchInput, FormField |
| Organism | Yes | UserList, DataTable, Forms |
| Page | Only for client-side mutations | useUsers.create() |
tsx1// ❌ BAD: Hook in atom 2export default function Button({ label }) { 3 const { isLoading } = useUIStore(); // DON'T 4 return <button>{label}</button>; 5} 6 7// ✅ GOOD: Props in atom 8export default function Button({ label, isLoading }) { 9 return <button disabled={isLoading}>{label}</button>; 10} 11 12// ✅ GOOD: Hook in organism 13export default function UserForm({ onSubmit }) { 14 const { isLoading, setLoading } = useUIStore(); 15 const [formData, setFormData] = useState({}); 16 17 const handleSubmit = async () => { 18 setLoading(true); 19 await onSubmit(formData); 20 setLoading(false); 21 }; 22 23 return ( 24 <form onSubmit={handleSubmit}> 25 <Input value={formData.name} onChange={(v) => setFormData({ ...formData, name: v })} /> 26 <Button isLoading={isLoading}>Submit</Button> 27 </form> 28 ); 29}
Server vs Client Data
tsx1// ✅ GOOD: Server Component fetches data directly 2// src/app/users/page.tsx 3async function getUsers() { 4 const res = await fetch("https://api.example.com/users"); 5 return res.json(); 6} 7 8export default async function UsersPage() { 9 const users = await getUsers(); 10 return <UserList users={users} />; 11} 12 13// ✅ GOOD: Client Component with hooks for interactivity 14// src/app/users/page.tsx 15"use client"; 16 17import { useUsers } from "@/hooks/users"; 18 19export default function UsersPage() { 20 const { data: users } = useUsers(); 21 return <UserList users={users} />; 22} 23 24// Component receives data as props 25export default function UserList({ users }: { users: User[] }) { 26 return <div>{users.map(...)}</div>; 27}
Client-Side Mutations
For mutations (create, update, delete), use hooks in Client Components:
tsx1// src/app/users/page.tsx 2"use client"; 3 4import { useUsers } from "@/hooks/users"; 5 6export default function UsersPage() { 7 const { data: users } = useUsers(); 8 const createUser = useUsers.create(); 9 const deleteUser = useUsers.delete(); 10 11 const handleCreate = (data: CreateUserInput) => { 12 createUser.mutate(data); 13 }; 14 15 const handleDelete = (id: string) => { 16 deleteUser.mutate(id); 17 }; 18 19 return ( 20 <div> 21 <UserForm onSubmit={handleCreate} /> 22 <UserList users={users} onDelete={handleDelete} /> 23 </div> 24 ); 25}
Important Notes
- Server Components fetch data directly - No hooks needed
- Client Components use hooks - For interactivity and mutations
- Props down, events up - Data flows down, actions bubble up via callbacks
- Keep atoms pure - No side effects, no hooks, just UI
- Organisms are the brain - Business logic lives here
- Test at the right level - Unit test atoms, integration test organisms
- Never use
any- Define proper types for all props and state - Handler naming -
handle*inside components,on*for props from parents - Prefer composition - Build complex UIs by combining simple components