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
txt
1Page (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.
tsx
1// 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}
tsx
1// 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.
tsx
1// 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}
tsx
1// 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.
tsx
1// 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.
tsx
1// 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:
tsx
1// 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
txt
1Server 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() |
tsx
1// ❌ 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
tsx
1// ✅ 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:
tsx
1// 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