📅 NGÀY 7: COMPONENT COMPOSITION - XÂY DỰNG UI LINH HOẠT
🎯 Mục tiêu học tập (5 phút)
Sau bài học hôm nay, bạn sẽ:
- [ ] Hiểu rõ Composition vs Inheritance trong React
- [ ] Nắm vững Children prop patterns (basic → advanced)
- [ ] Thành thạo Slot pattern (named children props)
- [ ] Sử dụng được Compound Components pattern
- [ ] Phân biệt Container vs Presentational components
- [ ] Biết khi nào dùng pattern nào cho từng use case
🤔 Kiểm tra đầu vào (5 phút)
Trước khi bắt đầu, hãy trả lời 3 câu hỏi này:
- Children prop là gì? Cho ví dụ cách dùng.
- Props có thể truyền những gì? (string, number, function...?)
- Component có thể return component khác không?
💡 Xem đáp án
- Children prop:
// Children là nội dung bên trong component tags
<Card>
<h3>Title</h3> {/* ← This is children */}
<p>Content</p>
</Card>;
function Card({ children }) {
return <div className='card'>{children}</div>;
}- Props có thể là:
<Component
text='string' // String
count={42} // Number
isActive={true} // Boolean
items={[1, 2, 3]} // Array
user={{ name: 'John' }} // Object
onClick={() => {}} // Function
icon={<Icon />} // JSX/Component
/>- Component return component:
function Parent() {
return <Child />; // ✅ Được!
}
function Wrapper() {
return (
<div>
<Header />
<Content />
<Footer />
</div>
);
}📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
Bạn cần xây dựng Dialog component. Có 3 loại:
// Type 1: Alert Dialog
<AlertDialog>
<h3>Warning!</h3>
<p>Are you sure?</p>
<button>OK</button>
</AlertDialog>
// Type 2: Confirm Dialog
<ConfirmDialog>
<h3>Delete Item?</h3>
<p>This cannot be undone</p>
<button>Cancel</button>
<button>Delete</button>
</ConfirmDialog>
// Type 3: Form Dialog
<FormDialog>
<h3>Login</h3>
<input type="text" />
<input type="password" />
<button>Submit</button>
</FormDialog>Cách tiếp cận cũ (Inheritance):
// ❌ Inheritance approach (không dùng trong React)
class Dialog {
render() {
/* base dialog */
}
}
class AlertDialog extends Dialog {
render() {
/* alert specific */
}
}
class ConfirmDialog extends Dialog {
render() {
/* confirm specific */
}
}
// Vấn đề:
// - Rigid hierarchy (cứng nhắc)
// - Hard to share behavior across branches
// - Tight couplingCách React (Composition):
// ✅ Composition approach
function Dialog({ children, title }) {
return (
<div className="dialog">
{title && <h3>{title}</h3>}
{children}
</div>
);
}
// Tái sử dụng linh hoạt
<Dialog title="Warning">
<p>Are you sure?</p>
<button>OK</button>
</Dialog>
<Dialog title="Login">
<input type="text" />
<button>Submit</button>
</Dialog>1.2 Composition vs Inheritance
React Philosophy: "Composition over Inheritance"
┌─────────────────────────────────────────┐
│ INHERITANCE (OOP) │
│ ┌───────────────────────────────┐ │
│ │ class Animal { │ │
│ │ eat() {} │ │
│ │ } │ │
│ │ ↑ │ │
│ │ class Dog extends Animal { │ │
│ │ bark() {} │ │
│ │ } │ │
│ └───────────────────────────────┘ │
│ • Tightly coupled (kết dính chặt) │
│ • Rigid hierarchy (phân cấp cứng) │
│ • Hard to change (khó thay đổi) │
└─────────────────────────────────────────┘
VS
┌─────────────────────────────────────────┐
│ COMPOSITION (React) │
│ ┌───────────────────────────────┐ │
│ │ <Layout> │ │
│ │ <Sidebar /> │ │
│ │ <Content> │ │
│ │ {children} │ │
│ │ </Content> │ │
│ │ </Layout> │ │
│ └───────────────────────────────┘ │
│ • Loosely coupled (liên kết lỏng) │
│ • Flexible (linh hoạt) │
│ • Easy to change (dễ thay đổi) │
└─────────────────────────────────────────┘🔑 Nguyên tắc:
- Components như LEGO blocks - lắp ghép linh hoạt
- Behavior qua Props - không phải inheritance
- Reuse qua Composition - không phải class hierarchy
1.3 Mental Model: Containment (Chứa đựng)
// Component là "container" chứa nội dung
function Box({ children, color }) {
return (
<div style={{ border: `2px solid ${color}`, padding: 20 }}>
{children} {/* Nội dung linh động */}
</div>
);
}
// Sử dụng
<Box color="blue">
<h3>Title</h3>
<p>Any content!</p>
<Button>Click</Button>
</Box>
<Box color="red">
<Image src="..." />
<Video src="..." />
</Box>Analogy (so sánh):
- Component = Cái hộp (container)
- Children = Nội dung (content)
- Props = Cấu hình hộp (box configuration)
1.4 Hiểu Lầm Phổ Biến
❌ Hiểu lầm 1: "Children chỉ có thể là JSX"
// ❌ SAI - nghĩ children chỉ là JSX
function Component({ children }) {
return <div>{children}</div>;
}
// ✅ ĐÚNG - children có thể là GÌ CŨNG ĐƯỢC!
<Component>Hello</Component> // String
<Component>{123}</Component> // Number
<Component>{true && <div>Yes</div>}</Component> // Conditional
<Component>{items.map(...)}</Component> // Array
<Component>{() => <div>Render</div>}</Component> // Function (Render prop)❌ Hiểu lầm 2: "Chỉ có 1 children prop"
// ❌ Limitation thinking
function Card({ children }) {
return (
<div className='card'>
{children} {/* Tất cả đều vào đây? */}
</div>
);
}
// ✅ BETTER - Multiple "slots" (nhiều vị trí)
function Card({ header, body, footer }) {
return (
<div className='card'>
<div className='card-header'>{header}</div>
<div className='card-body'>{body}</div>
<div className='card-footer'>{footer}</div>
</div>
);
}
// Usage - rõ ràng hơn
<Card
header={<h3>Title</h3>}
body={<p>Content</p>}
footer={<Button>OK</Button>}
/>;❌ Hiểu lầm 3: "Composition = phức tạp"
// ❌ Over-engineering
function SuperComplexComponent({
renderHeader,
renderBody,
renderFooter,
headerProps,
bodyProps,
footerProps,
onHeaderClick,
onBodyScroll,
...rest
}) {
// Too many abstractions!
}
// ✅ KISS (Keep It Simple, Stupid)
function Card({ children }) {
return <div className='card'>{children}</div>;
}
// Đơn giản, dễ hiểu, dễ maintain💻 PHẦN 2: LIVE CODING - PATTERNS CƠ BẢN (45 phút)
Demo 1: Basic Children Pattern ⭐
// Pattern 1: Simple Wrapper
function Container({ children }) {
return <div className='container'>{children}</div>;
}
// Usage
<Container>
<h1>Welcome</h1>
<p>This is content</p>
</Container>;// Pattern 2: Wrapper với Logic
function ProtectedRoute({ children, isAuthenticated }) {
if (!isAuthenticated) {
return <div>Please login!</div>;
}
return children;
}
// Usage
<ProtectedRoute isAuthenticated={user.loggedIn}>
<Dashboard />
</ProtectedRoute>;// Pattern 3: Wrapper với Enhancement (thêm tính năng)
function ErrorBoundary({ children }) {
// Giả sử có error state (sẽ học useState sau)
const hasError = false;
if (hasError) {
return <div>Something went wrong!</div>;
}
return children;
}
// Usage - wrap bất kỳ component nào
<ErrorBoundary>
<ComplexComponent />
</ErrorBoundary>;Demo 2: Slot Pattern (Named Children) ⭐⭐
// Problem: Children không rõ ràng vị trí
function Modal({ children }) {
return (
<div className='modal'>
{/* Header ở đâu? Body ở đâu? Footer ở đâu? */}
{children}
</div>
);
}
// ✅ Solution: Named slots (props)
function Modal({ title, content, actions }) {
return (
<div className='modal'>
<div className='modal-header'>
<h3>{title}</h3>
</div>
<div className='modal-body'>{content}</div>
<div className='modal-footer'>{actions}</div>
</div>
);
}
// Usage - rõ ràng từng phần
<Modal
title={<h3>Confirm Delete</h3>}
content={<p>Are you sure you want to delete this item?</p>}
actions={
<>
<button>Cancel</button>
<button>Delete</button>
</>
}
/>;Real-world Example: Layout với Sidebar
function PageLayout({ sidebar, main, footer }) {
return (
<div className='page-layout'>
<aside className='sidebar'>{sidebar}</aside>
<main className='main-content'>{main}</main>
{footer && <footer className='footer'>{footer}</footer>}
</div>
);
}
// Usage
<PageLayout
sidebar={
<nav>
<a href='/'>Home</a>
<a href='/about'>About</a>
</nav>
}
main={
<article>
<h1>Welcome</h1>
<p>Main content here...</p>
</article>
}
footer={<p>© 2025 Company</p>}
/>;Demo 3: Compound Components ⭐⭐⭐
Pattern mạnh nhất cho complex UIs!
// Compound Components - các components "biết" về nhau
function Tabs({ children }) {
// Giả sử activeTab state (sẽ dùng useState sau)
let activeTab = 'tab1';
return <div className='tabs'>{children}</div>;
}
// Sub-components
Tabs.List = function TabsList({ children }) {
return <div className='tabs-list'>{children}</div>;
};
Tabs.Tab = function Tab({ id, children }) {
const isActive = id === 'tab1'; // Simplified
return (
<button
className={`tab ${isActive ? 'active' : ''}`}
onClick={() => console.log('Switch to', id)}
>
{children}
</button>
);
};
Tabs.Panel = function TabPanel({ id, children }) {
const isActive = id === 'tab1'; // Simplified
if (!isActive) return null;
return <div className='tab-panel'>{children}</div>;
};
// Usage - API rõ ràng, linh hoạt
function App() {
return (
<Tabs>
<Tabs.List>
<Tabs.Tab id='tab1'>Profile</Tabs.Tab>
<Tabs.Tab id='tab2'>Settings</Tabs.Tab>
<Tabs.Tab id='tab3'>Notifications</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id='tab1'>
<h3>Profile Content</h3>
<p>Your profile information...</p>
</Tabs.Panel>
<Tabs.Panel id='tab2'>
<h3>Settings Content</h3>
<p>Your settings...</p>
</Tabs.Panel>
<Tabs.Panel id='tab3'>
<h3>Notifications Content</h3>
<p>Your notifications...</p>
</Tabs.Panel>
</Tabs>
);
}🔥 Lợi ích Compound Components:
- ✅ Flexible API - người dùng control structure
- ✅ Clear intent - rõ ràng từng phần làm gì
- ✅ Shared state - components "communicate" với nhau
- ✅ Customizable - dễ customize từng phần
Demo 4: Render Props Pattern ⭐⭐⭐
Legacy pattern nhưng quan trọng để hiểu
// Pattern: Component nhận function as child
function MouseTracker({ children }) {
// Giả sử có mouse position (sẽ dùng state + events)
const mouseX = 100;
const mouseY = 50;
return children({ x: mouseX, y: mouseY });
}
// Usage - function as children
<MouseTracker>
{({ x, y }) => (
<div>
<h3>Mouse Position</h3>
<p>
X: {x}, Y: {y}
</p>
</div>
)}
</MouseTracker>;
// Hoặc dùng prop 'render'
function MouseTracker({ render }) {
const mouseX = 100;
const mouseY = 50;
return render({ x: mouseX, y: mouseY });
}
<MouseTracker
render={({ x, y }) => (
<p>
Position: {x}, {y}
</p>
)}
/>;🎯 Khi nào dùng Render Props:
- ⚠️ Legacy code - code cũ còn dùng
- ⚠️ Library APIs - một số libraries dùng pattern này
- ✅ Modern alternative: Custom Hooks (sẽ học Ngày 24)
💻 PHẦN 2B: LIVE CODING - PATTERNS NÂNG CAO (45 phút)
Demo 5: Container vs Presentational ⭐⭐⭐
Separation of Concerns (Tách biệt logic & UI)
// ❌ BAD: Logic + UI lẫn lộn
function UserProfile() {
// Logic: fetch data, handle events
let user = null;
const fetchUser = async () => {
const response = await fetch('/api/user/123');
user = await response.json();
};
const handleUpdate = () => {
console.log('Update user');
};
// UI: render
return (
<div className='profile'>
<img
src={user?.avatar}
alt={user?.name}
/>
<h3>{user?.name}</h3>
<p>{user?.email}</p>
<button onClick={handleUpdate}>Update</button>
</div>
);
}
// Problem: Logic & UI coupled → hard to reuse, test// ✅ GOOD: Tách Container (logic) & Presentational (UI)
// Presentational Component - CHỈ UI, nhận props
function UserProfileView({ user, onUpdate }) {
return (
<div className='profile'>
<img
src={user.avatar}
alt={user.name}
/>
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={onUpdate}>Update</button>
</div>
);
}
// Container Component - CHỈ logic
function UserProfileContainer() {
// Logic: data fetching, state management
let user = {
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://i.pravatar.cc/150',
};
const handleUpdate = () => {
console.log('Update user');
};
// Render presentational component
return (
<UserProfileView
user={user}
onUpdate={handleUpdate}
/>
);
}
// Benefits:
// ✅ UserProfileView có thể reuse với data khác
// ✅ Dễ test UI riêng (pass mock data)
// ✅ Dễ style (UI designer chỉ cần sửa View)Pattern Summary:
| Component Type | Responsibility | Example |
|---|---|---|
| Container | Logic, data, state | UserProfileContainer |
| Presentational | UI, styling | UserProfileView |
Demo 6: Higher-Order Components (HOC) ⭐⭐
Pattern: Function nhận Component, return Enhanced Component
// HOC: withLoading - thêm loading state vào component
function withLoading(Component) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div className="loading">Loading...</div>;
}
return <Component {...props} />;
};
}
// Original component
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Enhanced component
const UserListWithLoading = withLoading(UserList);
// Usage
<UserListWithLoading
isLoading={true}
users={[]}
/>
// Shows "Loading..."
<UserListWithLoading
isLoading={false}
users={[{ id: 1, name: 'John' }]}
/>
// Shows user list⚠️ HOC là Legacy Pattern:
- Used heavily pre-Hooks (trước React 16.8)
- Modern alternative: Custom Hooks (sẽ học sau)
- Vẫn thấy trong legacy code & một số libraries
Demo 7: Specialization Pattern ⭐⭐
Tạo specialized version từ generic component
// Generic Button
function Button({ children, variant = 'default', size = 'medium', ...props }) {
const className = `btn btn-${variant} btn-${size}`;
return (
<button className={className} {...props}>
{children}
</button>
);
}
// Specialized Buttons - pre-configured
function PrimaryButton(props) {
return <Button variant="primary" {...props} />;
}
function DangerButton(props) {
return <Button variant="danger" {...props} />;
}
function LargeButton(props) {
return <Button size="large" {...props} />;
}
// Usage - cleaner, semantic
<PrimaryButton onClick={handleSave}>Save</PrimaryButton>
<DangerButton onClick={handleDelete}>Delete</DangerButton>
<LargeButton>Big Action</LargeButton>
// Thay vì
<Button variant="primary" onClick={handleSave}>Save</Button>
<Button variant="danger" onClick={handleDelete}>Delete</Button>
<Button size="large">Big Action</Button>Demo 8: React.Children API & cloneElement ⭐⭐⭐
Pattern: Manipulate children elements ( Thao tác với các phần tử children )
// React.Children API - các utility để làm việc với prop children
import React from 'react';
// 1. React.Children.map - Duyệt qua children (an toàn hơn children.map)
function List({ children }) {
// ❌ KHÔNG AN TOÀN: children.map() - lỗi nếu children chỉ là 1 phần tử
// children.map(child => ...) // Error nếu chỉ có 1 child!
// ✅ AN TOÀN: React.Children.map - luôn hoạt động
return (
<ul>
{React.Children.map(children, (child, index) => (
<li key={index}>{child}</li>
))}
</ul>
);
}
// Cách sử dụng
<List>
<span>Item 1</span>
<span>Item 2</span>
</List>;// 2. React.Children.count - Đếm số children
function ParentInfo({ children }) {
const count = React.Children.count(children);
return (
<div>
<p>This component has {count} children</p>
{children}
</div>
);
}
// Usage
<ParentInfo>
<div>Child 1</div>
<div>Child 2</div>
<div>Child 3</div>
</ParentInfo>;
// Output: "This component has 3 children"// 3. React.cloneElement - Clone element và thêm/ghi đè props
function Button({ children, ...props }) {
// Clone children và inject thêm props
const enhancedChildren = React.Children.map(children, (child) => {
// Kiểm tra child có phải là React element hợp lệ không
if (React.isValidElement(child)) {
// Clone và thêm className
return React.cloneElement(child, {
className: 'button-icon',
...child.props, // Giữ lại props gốc
});
}
return child;
});
return <button {...props}>{enhancedChildren}</button>;
}
// Cách sử dụng
<Button>
<span>Click</span> {/* Sẽ nhận className="button-icon" */}
</Button>;🔥 Pattern thực tế: Compound Components với Shared State
// Component Tabs - chia sẻ state activeTab xuống các children
function Tabs({ children }) {
let activeTab = 'tab1'; // State mô phỏng
// Inject activeTab vào children thông qua cloneElement
const enhancedChildren = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
// Clone child và truyền prop activeTab
return React.cloneElement(child, { activeTab });
}
return child;
});
return <div className='tabs'>{enhancedChildren}</div>;
}
Tabs.Tab = function Tab({ id, children, activeTab }) {
const isActive = activeTab === id;
return <button className={isActive ? 'active' : ''}>{children}</button>;
};
// Cách sử dụng - các Tab tự động nhận activeTab!
<Tabs>
<Tabs.Tab id='tab1'>Profile</Tabs.Tab>
<Tabs.Tab id='tab2'>Settings</Tabs.Tab>
</Tabs>;📚 Tóm tắt React.Children API:
| Method | Mục đích | Ví dụ |
|---|---|---|
React.Children.map(children, fn) | Duyệt qua children (an toàn) | Biến đổi / bọc children |
React.Children.forEach(children, fn) | Lặp qua children | Chỉ dùng cho side effects |
React.Children.count(children) | Đếm số children | Logic điều kiện |
React.Children.only(children) | Đảm bảo chỉ có 1 child | Component wrapper |
React.Children.toArray(children) | Chuyển thành array | Xử lý nâng cao |
⚠️ Khi nào nên dùng:
- ✅ Compound Components – inject shared state
- ✅ Layout components – bọc / tăng cường children
- ✅ HOC patterns – thêm props cho children
- ❌ Wrapper đơn giản – chỉ cần
{children} - ❌ Over-engineering – nguyên tắc KISS!
🎯 Ví dụ Dropdown (Hoàn chỉnh):
function Dropdown({ children }) {
let isOpen = false;
const toggle = () => {
isOpen = !isOpen;
console.log('isOpen:', isOpen);
};
return (
<div className='dropdown'>
{React.Children.map(children, (child) => {
// Inject props dựa trên loại component con
if (child.type === Dropdown.Trigger) {
return React.cloneElement(child, { onToggle: toggle });
}
if (child.type === Dropdown.Menu) {
return React.cloneElement(child, { isOpen });
}
return child;
})}
</div>
);
}
Dropdown.Trigger = function DropdownTrigger({ children, onToggle }) {
return (
<div
onClick={onToggle}
style={{ cursor: 'pointer' }}
>
{children}
</div>
);
};
Dropdown.Menu = function DropdownMenu({ isOpen, children }) {
if (!isOpen) return null;
return <div className='dropdown-menu'>{children}</div>;
};
/*
Luồng hoạt động:
1. Dropdown duyệt qua children
2. Gặp Trigger → inject onToggle
3. Gặp Menu → inject isOpen
4. Click Trigger → gọi toggle()
5. isOpen thay đổi (nhưng UI không cập nhật - cần useState!)
*/📝 Ý chính cần nhớ:
React.Children.mapan toàn hơnchildren.map()(xử lý được trường hợp chỉ có 1 child)React.cloneElementdùng để inject props vào children- Pattern phổ biến trong Compound Components
- Cần
useStateđể state thực sự hoạt động (Day 11)
🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Exercise 1: Card Component với Slots (15 phút)
🎯 Mục tiêu: Tạo flexible Card với named slots
⏱️ Thời gian: 15 phút
Requirements:
- Card component nhận: image, title, description, actions
- Tất cả slots đều optional
- Nếu không có image → không render image section
- Nếu không có actions → không render footer
/**
* 💡 Gợi ý:
* - Dùng props destructuring
* - Conditional rendering cho optional slots
* - Semantic HTML structure
*/
// 🎯 NHIỆM VỤ CỦA BẠN:
function Card({ image, title, description, actions }) {
return (
<div className='card'>
{/* TODO: Conditional render image */}
{/* TODO: Render title & description */}
{/* TODO: Conditional render actions */}
</div>
);
}
// Test cases
function App() {
return (
<div>
{/* Full card */}
<Card
image={
<img
src='https://picsum.photos/300/200'
alt='Product'
/>
}
title={<h3>Product Name</h3>}
description={<p>Product description goes here...</p>}
actions={
<>
<button>Buy Now</button>
<button>Add to Cart</button>
</>
}
/>
{/* Card without image */}
<Card
title={<h3>Text Only</h3>}
description={<p>No image card</p>}
/>
{/* Card without actions */}
<Card
image={
<img
src='https://picsum.photos/300/200'
alt='Info'
/>
}
title={<h3>Info Card</h3>}
description={<p>Read-only information</p>}
/>
</div>
);
}💡 Solution
function Card({ image, title, description, actions }) {
return (
<div className='card'>
{image && <div className='card-image'>{image}</div>}
<div className='card-content'>
{title && <div className='card-title'>{title}</div>}
{description && <div className='card-description'>{description}</div>}
</div>
{actions && <div className='card-actions'>{actions}</div>}
</div>
);
}
// Test
function App() {
return (
<div className='app'>
<h2>Card Component Examples</h2>
<div className='card-grid'>
<Card
image={
<img
src='https://picsum.photos/seed/1/300/200'
alt='Product'
/>
}
title={<h3>Premium Laptop</h3>}
description={
<p>
High-performance laptop for professionals. Latest specs, great
battery life.
</p>
}
actions={
<>
<button className='btn-primary'>Buy Now - $1299</button>
<button className='btn-secondary'>Add to Cart</button>
</>
}
/>
<Card
title={<h3>Announcement</h3>}
description={
<p>We're launching new features next week. Stay tuned!</p>
}
/>
<Card
image={
<img
src='https://picsum.photos/seed/2/300/200'
alt='Article'
/>
}
title={<h3>How to Learn React</h3>}
description={<p>A comprehensive guide to mastering React in 2025.</p>}
/>
</div>
</div>
);
}📚 Key Learning:
- Named slots rõ ràng hơn single children
- Conditional rendering cho flexibility
- Semantic HTML structure
⭐⭐ Exercise 2: Accordion Component (25 phút)
🎯 Mục tiêu: Tạo Accordion với Compound Components
⏱️ Thời gian: 25 phút
Scenario: Tạo FAQ accordion (click để expand/collapse)
Requirements:
- Accordion container
- Accordion.Item - mỗi item độc lập
- Accordion.Header - clickable để toggle
- Accordion.Content - nội dung expand/collapse
- Click header → toggle content visibility
// 🎯 NHIỆM VỤ:
function Accordion({ children }) {
return <div className='accordion'>{children}</div>;
}
Accordion.Item = function AccordionItem({ children, id }) {
// TODO: Track if this item is open
let isOpen = false;
return (
<div className={`accordion-item ${isOpen ? 'open' : ''}`}>{children}</div>
);
};
Accordion.Header = function AccordionHeader({ children }) {
const handleClick = () => {
console.log('Toggle accordion');
// TODO: Toggle isOpen
};
return (
<div
className='accordion-header'
onClick={handleClick}
>
{children}
<span className='accordion-icon'>
{/* TODO: Show ▼ or ▲ based on isOpen */}
</span>
</div>
);
};
Accordion.Content = function AccordionContent({ children }) {
// TODO: Only show if isOpen
return <div className='accordion-content'>{children}</div>;
};
// Test
function App() {
return (
<Accordion>
<Accordion.Item id='item1'>
<Accordion.Header>
<h3>What is React?</h3>
</Accordion.Header>
<Accordion.Content>
<p>React is a JavaScript library for building user interfaces.</p>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item id='item2'>
<Accordion.Header>
<h3>Why use React?</h3>
</Accordion.Header>
<Accordion.Content>
<p>
React makes it easy to build complex UIs with reusable components.
</p>
</Accordion.Content>
</Accordion.Item>
</Accordion>
);
}💡 Solution
function Accordion({ children }) {
return <div className='accordion'>{children}</div>;
}
Accordion.Item = function AccordionItem({ children, id }) {
// Simulated state (will use useState on Day 11)
let isOpen = false;
const handleToggle = () => {
isOpen = !isOpen;
console.log(`Item ${id} is now ${isOpen ? 'open' : 'closed'}`);
};
return (
<div
className={`accordion-item ${isOpen ? 'open' : ''}`}
data-id={id}
>
{/* Pass toggle function to children via context pattern */}
{children}
</div>
);
};
Accordion.Header = function AccordionHeader({ children, onClick }) {
return (
<div
className='accordion-header'
onClick={onClick}
>
<div className='accordion-title'>{children}</div>
<span className='accordion-icon'>▼</span>
</div>
);
};
Accordion.Content = function AccordionContent({ children, isOpen }) {
if (!isOpen) return null;
return <div className='accordion-content'>{children}</div>;
};
// Better implementation with shared state
function AccordionDemo() {
// Simplified: track which item is open
let openItem = 'item1';
const createToggleHandler = (id) => () => {
openItem = openItem === id ? null : id;
console.log('Open item:', openItem);
};
const faqs = [
{
id: 'item1',
question: 'What is React?',
answer:
'React is a JavaScript library for building user interfaces, maintained by Meta.',
},
{
id: 'item2',
question: 'Why use React?',
answer:
'React makes it easy to build complex UIs with reusable components and efficient rendering.',
},
{
id: 'item3',
question: 'What are components?',
answer:
'Components are independent, reusable pieces of UI that can be composed together.',
},
];
return (
<div className='faq-container'>
<h2>Frequently Asked Questions</h2>
<Accordion>
{faqs.map((faq) => {
const isOpen = openItem === faq.id;
return (
<Accordion.Item
key={faq.id}
id={faq.id}
>
<Accordion.Header onClick={createToggleHandler(faq.id)}>
<h3>{faq.question}</h3>
</Accordion.Header>
<Accordion.Content isOpen={isOpen}>
<p>{faq.answer}</p>
</Accordion.Content>
</Accordion.Item>
);
})}
</Accordion>
<div className='note'>
<strong>Note:</strong> State won't persist (need useState - Day 11).
Current open: <strong>{openItem || 'None'}</strong>
</div>
</div>
);
}📚 Key Concepts:
- Compound components pattern
- Shared state between sibling components
- Flexible, declarative API
- Easy to customize individual items
⭐⭐⭐ Exercise 3: Modal System (40 phút)
🎯 Mục tiêu: Tạo flexible Modal với composition
⏱️ Thời gian: 40 phút
📋 Product Requirements:
User Story: "Là developer, tôi muốn Modal component linh hoạt để tạo các loại dialog khác nhau mà không cần viết lại code."
✅ Acceptance Criteria:
- [ ] Modal với backdrop (click backdrop → close)
- [ ] Modal.Header với title và close button
- [ ] Modal.Body cho content
- [ ] Modal.Footer cho actions
- [ ] Keyboard support (Esc → close)
- [ ] Flexible - có thể skip header/footer
// 🎯 NHIỆM VỤ:
// Main Modal container
function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null;
const handleBackdropClick = (event) => {
// TODO: Close modal if click backdrop (not content)
};
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onClose();
}
};
// TODO: Add keyboard listener (simplified)
return (
<>
<div
className='modal-backdrop'
onClick={handleBackdropClick}
/>
<div className='modal-wrapper'>
<div
className='modal-content'
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
</>
);
}
// Sub-components
Modal.Header = function ModalHeader({ children, onClose }) {
return (
<div className='modal-header'>
<div className='modal-title'>{children}</div>
{onClose && (
<button
className='modal-close'
onClick={onClose}
>
✕
</button>
)}
</div>
);
};
Modal.Body = function ModalBody({ children }) {
return <div className='modal-body'>{children}</div>;
};
Modal.Footer = function ModalFooter({ children }) {
return <div className='modal-footer'>{children}</div>;
};
// Test cases
function App() {
let isOpen = true; // Will use useState later
const handleClose = () => {
isOpen = false;
console.log('Modal closed');
};
return (
<div className='app'>
<button
onClick={() => {
isOpen = true;
}}
>
Open Modal
</button>
{/* Example 1: Full modal */}
<Modal
isOpen={isOpen}
onClose={handleClose}
>
<Modal.Header onClose={handleClose}>
<h3>Confirm Action</h3>
</Modal.Header>
<Modal.Body>
<p>Are you sure you want to delete this item?</p>
<p>This action cannot be undone.</p>
</Modal.Body>
<Modal.Footer>
<button onClick={handleClose}>Cancel</button>
<button className='btn-danger'>Delete</button>
</Modal.Footer>
</Modal>
{/* Example 2: Modal without footer */}
<Modal
isOpen={isOpen}
onClose={handleClose}
>
<Modal.Header onClose={handleClose}>
<h3>Information</h3>
</Modal.Header>
<Modal.Body>
<p>This is just informational content.</p>
</Modal.Body>
</Modal>
</div>
);
}💡 Solution
function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null;
const handleBackdropClick = () => {
onClose();
};
const handleContentClick = (event) => {
event.stopPropagation(); // Prevent close when clicking content
};
// Keyboard support
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
onClose();
}
};
// Add/remove keyboard listener (simplified - in real app use useEffect)
if (typeof window !== 'undefined') {
window.addEventListener('keydown', handleKeyDown);
}
return (
<>
<div
className='modal-backdrop'
onClick={handleBackdropClick}
/>
<div className='modal-wrapper'>
<div
className='modal-content'
onClick={handleContentClick}
>
{children}
</div>
</div>
</>
);
}
Modal.Header = function ModalHeader({ children, onClose, showClose = true }) {
return (
<div className='modal-header'>
<div className='modal-title'>{children}</div>
{showClose && onClose && (
<button
className='modal-close'
onClick={onClose}
aria-label='Close modal'
>
✕
</button>
)}
</div>
);
};
Modal.Body = function ModalBody({ children }) {
return <div className='modal-body'>{children}</div>;
};
Modal.Footer = function ModalFooter({ children, align = 'right' }) {
return <div className={`modal-footer modal-footer-${align}`}>{children}</div>;
};
// Demo with different modal types
function ModalDemo() {
let confirmModalOpen = false;
let infoModalOpen = false;
let formModalOpen = false;
const handleOpenConfirm = () => {
confirmModalOpen = true;
console.log('Confirm modal opened');
};
const handleOpenInfo = () => {
infoModalOpen = true;
console.log('Info modal opened');
};
const handleOpenForm = () => {
formModalOpen = true;
console.log('Form modal opened');
};
const handleClose = (modalName) => () => {
console.log(`Closed ${modalName} modal`);
// Will update state with useState on Day 11
};
return (
<div className='app'>
<h2>Modal Component Demo</h2>
<div className='button-group'>
<button onClick={handleOpenConfirm}>Open Confirm Modal</button>
<button onClick={handleOpenInfo}>Open Info Modal</button>
<button onClick={handleOpenForm}>Open Form Modal</button>
</div>
{/* Confirm Modal */}
<Modal
isOpen={confirmModalOpen}
onClose={handleClose('confirm')}
>
<Modal.Header onClose={handleClose('confirm')}>
<h3>⚠️ Confirm Delete</h3>
</Modal.Header>
<Modal.Body>
<p>Are you sure you want to delete this item?</p>
<p className='warning-text'>This action cannot be undone!</p>
</Modal.Body>
<Modal.Footer>
<button
className='btn-secondary'
onClick={handleClose('confirm')}
>
Cancel
</button>
<button
className='btn-danger'
onClick={() => {
console.log('Item deleted');
handleClose('confirm')();
}}
>
Delete
</button>
</Modal.Footer>
</Modal>
{/* Info Modal - No footer */}
<Modal
isOpen={infoModalOpen}
onClose={handleClose('info')}
>
<Modal.Header onClose={handleClose('info')}>
<h3>ℹ️ Information</h3>
</Modal.Header>
<Modal.Body>
<p>This is an informational message.</p>
<ul>
<li>Point 1: Important info</li>
<li>Point 2: More details</li>
<li>Point 3: Additional context</li>
</ul>
</Modal.Body>
</Modal>
{/* Form Modal */}
<Modal
isOpen={formModalOpen}
onClose={handleClose('form')}
>
<Modal.Header onClose={handleClose('form')}>
<h3>📝 Create New Item</h3>
</Modal.Header>
<Modal.Body>
<form className='modal-form'>
<div className='form-group'>
<label htmlFor='item-name'>Item Name</label>
<input
type='text'
id='item-name'
placeholder='Enter name...'
/>
</div>
<div className='form-group'>
<label htmlFor='item-desc'>Description</label>
<textarea
id='item-desc'
rows='4'
placeholder='Enter description...'
/>
</div>
</form>
</Modal.Body>
<Modal.Footer>
<button
className='btn-secondary'
onClick={handleClose('form')}
>
Cancel
</button>
<button
className='btn-primary'
onClick={() => {
console.log('Form submitted');
handleClose('form')();
}}
>
Create
</button>
</Modal.Footer>
</Modal>
<div className='note'>
<strong>Note:</strong> Modal state won't persist (need useState - Day
11). Press <kbd>Esc</kbd> to close modal.
</div>
</div>
);
}📚 Production Features:
- Backdrop click → close modal
- Esc key → close modal
- Click content → không close (stopPropagation)
- Flexible composition → có thể skip header/footer
- Accessibility → aria-label, keyboard support
⭐⭐⭐⭐ Exercise 4: Page Layout System (60 phút)
🎯 Mục tiêu: Tạo layout system với nested composition
⏱️ Thời gian: 60 phút
🏗️ PHASE 1: Research & Design (20 phút)
Thiết kế một layout system cho app (giống Admin Dashboard).
Features cần có:
- Page wrapper (full height)
- Header (fixed top)
- Sidebar (collapsible)
- Main content area (scrollable)
- Footer (optional)
🤔 DESIGN DECISIONS:
Layout Structure:
- Option A: Flat structure với positioning
- Option B: Nested structure với composition
- Decision: ?
Sidebar Behavior:
- Always visible?
- Collapsible?
- Responsive (hide on mobile)?
ADR:
## Decision: Layout System Architecture
### Context
Need flexible layout system for app pages.
### Decision
[Your choice]
### Rationale
1. [Reason 1]
2. [Reason 2]
### Implementation
[Approach]💻 PHASE 2: Implementation (30 phút)
// 🎯 NHIỆM VỤ:
// Main Layout component
function AppLayout({ children }) {
return <div className='app-layout'>{children}</div>;
}
// Sub-components
AppLayout.Header = function LayoutHeader({ children }) {
return <header className='layout-header'>{children}</header>;
};
AppLayout.Sidebar = function LayoutSidebar({ children, isCollapsed }) {
return (
<aside className={`layout-sidebar ${isCollapsed ? 'collapsed' : ''}`}>
{children}
</aside>
);
};
AppLayout.Main = function LayoutMain({ children }) {
return <main className='layout-main'>{children}</main>;
};
AppLayout.Footer = function LayoutFooter({ children }) {
return <footer className='layout-footer'>{children}</footer>;
};
// Usage
function DashboardPage() {
let sidebarCollapsed = false;
const handleToggleSidebar = () => {
sidebarCollapsed = !sidebarCollapsed;
console.log('Sidebar:', sidebarCollapsed ? 'collapsed' : 'expanded');
};
return (
<AppLayout>
<AppLayout.Header>
<div className='header-content'>
<button onClick={handleToggleSidebar}>☰</button>
<h1>Dashboard</h1>
<div className='header-actions'>
<button>Profile</button>
<button>Logout</button>
</div>
</div>
</AppLayout.Header>
<div className='layout-body'>
<AppLayout.Sidebar isCollapsed={sidebarCollapsed}>
{/* TODO: Navigation menu */}
</AppLayout.Sidebar>
<AppLayout.Main>{/* TODO: Page content */}</AppLayout.Main>
</div>
<AppLayout.Footer>
<p>© 2025 Company Name</p>
</AppLayout.Footer>
</AppLayout>
);
}💡 Solution
// Complete Layout System
function AppLayout({ children }) {
return <div className='app-layout'>{children}</div>;
}
AppLayout.Header = function LayoutHeader({ children }) {
return <header className='layout-header'>{children}</header>;
};
AppLayout.Sidebar = function LayoutSidebar({ children, isCollapsed = false }) {
return (
<aside className={`layout-sidebar ${isCollapsed ? 'collapsed' : ''}`}>
<div className='sidebar-content'>{children}</div>
</aside>
);
};
AppLayout.Main = function LayoutMain({ children }) {
return (
<main className='layout-main'>
<div className='main-content'>{children}</div>
</main>
);
};
AppLayout.Footer = function LayoutFooter({ children }) {
return <footer className='layout-footer'>{children}</footer>;
};
// Navigation component for sidebar
function NavMenu() {
const menuItems = [
{ icon: '📊', label: 'Dashboard', path: '/dashboard' },
{ icon: '👥', label: 'Users', path: '/users' },
{ icon: '📦', label: 'Products', path: '/products' },
{ icon: '📈', label: 'Analytics', path: '/analytics' },
{ icon: '⚙️', label: 'Settings', path: '/settings' },
];
return (
<nav className='nav-menu'>
{menuItems.map((item) => (
<a
key={item.path}
href={item.path}
className='nav-item'
onClick={(e) => {
e.preventDefault();
console.log('Navigate to:', item.path);
}}
>
<span className='nav-icon'>{item.icon}</span>
<span className='nav-label'>{item.label}</span>
</a>
))}
</nav>
);
}
// Complete Dashboard Demo
function DashboardPage() {
let sidebarCollapsed = false;
const handleToggleSidebar = () => {
sidebarCollapsed = !sidebarCollapsed;
console.log('Sidebar:', sidebarCollapsed ? 'collapsed' : 'expanded');
};
return (
<AppLayout>
{/* Header */}
<AppLayout.Header>
<div className='header-content'>
<div className='header-left'>
<button
className='sidebar-toggle'
onClick={handleToggleSidebar}
aria-label='Toggle sidebar'
>
☰
</button>
<h1 className='header-title'>My Dashboard</h1>
</div>
<div className='header-right'>
<input
type='search'
placeholder='Search...'
className='header-search'
/>
<button className='header-btn'>🔔</button>
<button className='header-btn'>👤 Profile</button>
</div>
</div>
</AppLayout.Header>
{/* Body with Sidebar + Main */}
<div className='layout-body'>
<AppLayout.Sidebar isCollapsed={sidebarCollapsed}>
<div className='sidebar-header'>
<h2>{sidebarCollapsed ? 'M' : 'Menu'}</h2>
</div>
<NavMenu />
</AppLayout.Sidebar>
<AppLayout.Main>
<div className='page-header'>
<h2>Welcome Back!</h2>
<p>Here's what's happening with your projects today.</p>
</div>
{/* Dashboard cards */}
<div className='dashboard-grid'>
{[
{ title: 'Total Users', value: '1,234', trend: '+12%' },
{ title: 'Revenue', value: '$45,678', trend: '+8%' },
{ title: 'Orders', value: '567', trend: '-3%' },
{ title: 'Visitors', value: '8,901', trend: '+15%' },
].map((stat, index) => (
<div
key={index}
className='stat-card'
>
<h3 className='stat-title'>{stat.title}</h3>
<p className='stat-value'>{stat.value}</p>
<span
className={`stat-trend ${
stat.trend.startsWith('+') ? 'positive' : 'negative'
}`}
>
{stat.trend}
</span>
</div>
))}
</div>
{/* Recent activity */}
<div className='activity-section'>
<h3>Recent Activity</h3>
<div className='activity-list'>
{[
'User John Doe registered',
'New order #1234 received',
'Product "Laptop" updated',
'Payment processed successfully',
].map((activity, index) => (
<div
key={index}
className='activity-item'
>
<span className='activity-dot'></span>
<p>{activity}</p>
<span className='activity-time'>{index + 1}h ago</span>
</div>
))}
</div>
</div>
</AppLayout.Main>
</div>
{/* Footer */}
<AppLayout.Footer>
<div className='footer-content'>
<p>© 2025 My Company. All rights reserved.</p>
<div className='footer-links'>
<a href='/privacy'>Privacy</a>
<a href='/terms'>Terms</a>
<a href='/contact'>Contact</a>
</div>
</div>
</AppLayout.Footer>
<div className='note'>
<strong>Note:</strong> Sidebar state won't persist (need useState - Day
11).
</div>
</AppLayout>
);
}📚 Architecture Benefits:
- Flexible composition - bất kỳ layout nào
- Clear structure - dễ đọc, dễ maintain
- Reusable - dùng cho nhiều pages
- Responsive - dễ thêm responsive logic sau
- Semantic - HTML tags đúng nghĩa
⭐⭐⭐⭐⭐ Exercise 5: Theme Provider System (90 phút)
🎯 Mục tiêu: Tạo theme system với context-like pattern
⏱️ Thời gian: 90 phút
📋 Feature Specification:
Tạo ThemeProvider cho Dark/Light mode:
- ThemeProvider wrap toàn app
- Theme data truyền xuống children
- Components access theme để styling
- Toggle button switch themes
Requirements:
- ThemeProvider component
- Theme object (colors, fonts, spacing)
- Các components dùng theme
- Toggle functionality
// Theme definitions
const themes = {
light: {
colors: {
primary: '#1976d2',
background: '#ffffff',
text: '#333333',
border: '#e0e0e0',
},
fonts: {
body: 'Arial, sans-serif',
heading: 'Georgia, serif',
},
},
dark: {
colors: {
primary: '#90caf9',
background: '#121212',
text: '#ffffff',
border: '#333333',
},
fonts: {
body: 'Arial, sans-serif',
heading: 'Georgia, serif',
},
},
};
// 🎯 NHIỆM VỤ:
function ThemeProvider({ children, theme }) {
// TODO: Provide theme to children
// (Will use Context on Day 36)
return <div data-theme={theme}>{children}</div>;
}
// Components that use theme
function ThemedButton({ children, ...props }) {
// TODO: Access theme
const theme = themes.light; // Hardcoded for now
const style = {
backgroundColor: theme.colors.primary,
color: '#fff',
border: 'none',
padding: '10px 20px',
borderRadius: '4px',
};
return (
<button
style={style}
{...props}
>
{children}
</button>
);
}
function ThemedCard({ children }) {
const theme = themes.light;
const style = {
backgroundColor: theme.colors.background,
color: theme.colors.text,
border: `1px solid ${theme.colors.border}`,
padding: '20px',
borderRadius: '8px',
};
return <div style={style}>{children}</div>;
}
// App
function App() {
let currentTheme = 'light';
const toggleTheme = () => {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
console.log('Theme:', currentTheme);
};
return (
<ThemeProvider theme={currentTheme}>
<div className='app'>
<button onClick={toggleTheme}>
Toggle Theme (Current: {currentTheme})
</button>
<ThemedCard>
<h2>Themed Components</h2>
<p>This card adapts to the theme.</p>
<ThemedButton>Click Me</ThemedButton>
</ThemedCard>
</div>
</ThemeProvider>
);
}💡 Solution (Simplified without Context)
// Theme definitions
const themes = {
light: {
colors: {
primary: '#1976d2',
secondary: '#dc004e',
background: '#ffffff',
surface: '#f5f5f5',
text: '#333333',
textSecondary: '#666666',
border: '#e0e0e0',
},
fonts: {
body: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
heading: 'Georgia, serif',
},
spacing: {
small: '8px',
medium: '16px',
large: '24px',
},
},
dark: {
colors: {
primary: '#90caf9',
secondary: '#f48fb1',
background: '#121212',
surface: '#1e1e1e',
text: '#ffffff',
textSecondary: '#b0b0b0',
border: '#333333',
},
fonts: {
body: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
heading: 'Georgia, serif',
},
spacing: {
small: '8px',
medium: '16px',
large: '24px',
},
},
};
// Theme Provider (simplified - will use Context later)
function ThemeProvider({ children, themeName }) {
const theme = themes[themeName] || themes.light;
// Apply theme to root element
const rootStyle = {
backgroundColor: theme.colors.background,
color: theme.colors.text,
fontFamily: theme.fonts.body,
minHeight: '100vh',
transition: 'all 0.3s ease',
};
return (
<div
style={rootStyle}
data-theme={themeName}
>
{/* Pass theme to children via props (simplified) */}
{typeof children === 'function' ? children(theme) : children}
</div>
);
}
// Themed components
function ThemedButton({ children, variant = 'primary', theme, ...props }) {
const style = {
backgroundColor: theme.colors[variant],
color: variant === 'primary' ? '#fff' : theme.colors.text,
border: 'none',
padding: `${theme.spacing.small} ${theme.spacing.medium}`,
borderRadius: '4px',
fontFamily: theme.fonts.body,
cursor: 'pointer',
transition: 'all 0.2s',
};
return (
<button
style={style}
{...props}
>
{children}
</button>
);
}
function ThemedCard({ children, theme }) {
const style = {
backgroundColor: theme.colors.surface,
color: theme.colors.text,
border: `1px solid ${theme.colors.border}`,
padding: theme.spacing.large,
borderRadius: '8px',
marginBottom: theme.spacing.medium,
};
return <div style={style}>{children}</div>;
}
function ThemedInput({ placeholder, theme, ...props }) {
const style = {
backgroundColor: theme.colors.background,
color: theme.colors.text,
border: `2px solid ${theme.colors.border}`,
padding: theme.spacing.small,
borderRadius: '4px',
fontFamily: theme.fonts.body,
width: '100%',
fontSize: '14px',
};
return (
<input
style={style}
placeholder={placeholder}
{...props}
/>
);
}
// Demo App
function ThemeDemo() {
let currentTheme = 'light';
const toggleTheme = () => {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
console.log('Switched to theme:', currentTheme);
};
return (
<ThemeProvider themeName={currentTheme}>
{(theme) => (
<div style={{ padding: theme.spacing.large }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: theme.spacing.large,
}}
>
<h1
style={{
fontFamily: theme.fonts.heading,
margin: 0,
}}
>
Theme System Demo
</h1>
<ThemedButton
theme={theme}
onClick={toggleTheme}
>
🌓 Toggle Theme (Current: {currentTheme})
</ThemedButton>
</div>
<ThemedCard theme={theme}>
<h2
style={{
fontFamily: theme.fonts.heading,
marginTop: 0,
}}
>
Welcome!
</h2>
<p style={{ color: theme.colors.textSecondary }}>
This is a themed card component. All colors and spacing come from
the theme system.
</p>
<div style={{ marginTop: theme.spacing.medium }}>
<ThemedButton
theme={theme}
variant='primary'
>
Primary Action
</ThemedButton>{' '}
<ThemedButton
theme={theme}
variant='secondary'
>
Secondary Action
</ThemedButton>
</div>
</ThemedCard>
<ThemedCard theme={theme}>
<h3 style={{ marginTop: 0 }}>Form Example</h3>
<div style={{ marginBottom: theme.spacing.medium }}>
<label
style={{
display: 'block',
marginBottom: theme.spacing.small,
}}
>
Username
</label>
<ThemedInput
theme={theme}
placeholder='Enter username...'
/>
</div>
<div style={{ marginBottom: theme.spacing.medium }}>
<label
style={{
display: 'block',
marginBottom: theme.spacing.small,
}}
>
Email
</label>
<ThemedInput
theme={theme}
placeholder='Enter email...'
/>
</div>
<ThemedButton theme={theme}>Submit</ThemedButton>
</ThemedCard>
<div
style={{
padding: theme.spacing.medium,
background: theme.colors.surface,
borderRadius: '8px',
border: `1px solid ${theme.colors.border}`,
}}
>
<strong>Note:</strong> Theme won't persist (need useState - Day 11).
On Day 36, we'll learn Context API to properly share theme across
components!
</div>
</div>
)}
</ThemeProvider>
);
}📚 Key Concepts:
- Theme object - centralized design tokens
- Provider pattern - wrap app to provide theme
- Render props - pass theme to children via function
- Inline styles - dynamic styling based on theme
- Future: Context API will make this much cleaner!
📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh: Composition Patterns
| Pattern | Use Case | Pros ✅ | Cons ❌ | Example |
|---|---|---|---|---|
| Children Prop | Simple wrappers | • Simple • Flexible content | • No structure • Hard to target specific parts | <Card>{content}</Card> |
| Named Slots | Structured layouts | • Clear structure • Easy to style parts | • More props • Verbose | <Card header={...} body={...} /> |
| Compound Components | Complex UIs | • Flexible API • Shared state • Clear hierarchy | • More complex • Learning curve | <Tabs><Tabs.Tab /></Tabs> |
| Render Props | Logic reuse | • Maximum flexibility • Access to internals | • Complex syntax • Callback hell | <Mouse render={({x,y}) => ...} /> |
| HOC | Cross-cutting concerns | • Reuse logic • Composition | • Props collision • Wrapper hell | withAuth(Component) |
Decision Tree: Chọn Pattern
Cần compose components?
│
├─ Wrapper đơn giản, nội dung linh động?
│ └─ ✅ CHILDREN PROP
│ <Container>{anything}</Container>
│
├─ Layout với nhiều vùng rõ ràng?
│ ├─ 2-3 slots cố định?
│ │ └─ ✅ NAMED SLOTS (Props)
│ │ <Modal title={...} content={...} actions={...} />
│ │
│ └─ Flexible structure, nhiều sub-components?
│ └─ ✅ COMPOUND COMPONENTS
│ <Tabs><Tabs.Tab /><Tabs.Panel /></Tabs>
│
├─ Cần share logic (không phải UI)?
│ ├─ Modern codebase?
│ │ └─ ✅ CUSTOM HOOKS (Day 24)
│ │
│ └─ Legacy/Library code?
│ └─ ⚠️ Render Props hoặc HOC
│
└─ Tách logic khỏi presentation?
└─ ✅ CONTAINER/PRESENTATIONAL
Container (logic) + View (UI)🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Props Drilling Hell ❌
// 🐛 CODE BỊ LỖI:
function App() {
const user = { name: 'John', role: 'admin' };
return <Dashboard user={user} />;
}
function Dashboard({ user }) {
return (
<div>
<Header user={user} />
<Sidebar user={user} />
<Content user={user} />
</div>
);
}
function Header({ user }) {
return <TopBar user={user} />;
}
function TopBar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <div>{user.name}</div>; // Finally used here!
}
// Props drill qua 4 levels chỉ để đến UserMenu!❓ Câu hỏi:
- Vấn đề gì với code này?
- Nếu thêm prop mới, phải sửa ở đâu?
- Giải pháp?
💡 Giải đáp
1. Vấn đề:
- Props drilling - truyền props qua nhiều levels
- Intermediate components (Dashboard, Header, TopBar) không dùng
usernhưng phải nhận và forward - Hard to maintain - thêm prop = sửa nhiều chỗ
- Tight coupling - tất cả components biết về user structure
2. Nếu thêm prop:
// Phải sửa 5 chỗ!
<Dashboard user={user} theme={theme} />
function Dashboard({ user, theme }) { ... }
<Header user={user} theme={theme} />
function Header({ user, theme }) { ... }
...3. Giải pháp:
Solution 1: Composition (Di chuyển UserMenu lên)
function App() {
const user = { name: 'John', role: 'admin' };
return (
<Dashboard>
<Header>
<UserMenu user={user} /> {/* Trực tiếp! */}
</Header>
<Sidebar />
<Content />
</Dashboard>
);
}
// Không còn props drilling!Solution 2: Context API (Day 36)
// Sẽ học sau - share data without drilling
<UserContext.Provider value={user}>
<Dashboard /> {/* user available ở mọi nơi */}
</UserContext.Provider>Solution 3: Specialized Components
// Pre-configure components
function AdminDashboard() {
const user = { name: 'John', role: 'admin' };
return (
<Dashboard
header={<AdminHeader user={user} />}
sidebar={<AdminSidebar user={user} />}
content={<AdminContent user={user} />}
/>
);
}Bug 2: Children Overwrite ❌
// 🐛 CODE BỊ LỖI:
function Card({ children, title }) {
return (
<div className='card'>
<h3>{title}</h3>
{/* Always overwrite children */}
<div className='card-body'>
<p>Fixed content here</p>
</div>
</div>
);
}
// Usage - children bị ignore!
<Card title='My Card'>
<p>This content is LOST!</p>
<button>Click me</button>
</Card>;
// Output: chỉ thấy "Fixed content here"❓ Câu hỏi:
- Tại sao children không hiển thị?
- Làm sao fix?
💡 Giải đáp
1. Tại sao:
- Component nhận
childrenprop nhưng không render nó! - Hard-coded content
<p>Fixed content here</p>thay thế children
2. Fix:
// ✅ CORRECT: Render children
function Card({ children, title }) {
return (
<div className='card'>
<h3>{title}</h3>
<div className='card-body'>
{children} {/* Render children! */}
</div>
</div>
);
}
// ✅ ALTERNATIVE: Combine fixed + dynamic content
function Card({ children, title }) {
return (
<div className='card'>
<h3>{title}</h3>
<div className='card-body'>
<p className='card-description'>Welcome to the card!</p>
{children} {/* Add children sau fixed content */}
</div>
</div>
);
}Bug 3: Compound Components Without Shared State ❌
// 🐛 CODE BỊ LỖI:
function Tabs({ children }) {
return <div className="tabs">{children}</div>;
}
Tabs.Tab = function Tab({ id, children }) {
// Mỗi Tab có state riêng!
let isActive = false;
return (
<button
className={isActive ? 'active' : ''}
onClick={() => { isActive = true; }}
>
{children}
</button>
);
}
Tabs.Panel = function Panel({ id, children }) {
// Panel không biết Tab nào active!
let isActive = false;
return isActive ? <div>{children}</div> : null;
}
// Usage - không hoạt động!
<Tabs>
<Tabs.Tab id="1">Tab 1</Tabs.Tab>
<Tabs.Tab id="2">Tab 2</Tabs.Tab>
<Tabs.Panel id="1">Content 1</Tabs.Panel>
<Tabs.Panel id="2">Content 2</Tabs.Panel>
</Tabs>❓ Câu hỏi:
- Vấn đề gì?
- Tab và Panel cần gì để "communicate"?
- Giải pháp?
💡 Giải đáp
1. Vấn đề:
- Tab và Panel không share state
- Click Tab không update Panel
- Mỗi component isolated, không biết về nhau
2. Cần gì:
- Shared state - biết tab nào đang active
- Communication - Tab click → Panel update
3. Giải pháp:
Temporary (without state):
// Pass active state qua props
<Tabs activeId='1'>
<Tabs.Tab id='1'>Tab 1</Tabs.Tab>
<Tabs.Tab id='2'>Tab 2</Tabs.Tab>
<Tabs.Panel
id='1'
activeId='1'
>
Content 1
</Tabs.Panel>
<Tabs.Panel
id='2'
activeId='1'
>
Content 2
</Tabs.Panel>
</Tabs>Proper solution (Day 11 - useState):
function Tabs({ children }) {
// Shared state for all Tabs/Panels
const [activeTab, setActiveTab] = useState('1');
// Pass state to children via Context (Day 36)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className='tabs'>{children}</div>
</TabsContext.Provider>
);
}For now: Accept that compound components need state management (coming soon!)
✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu Composition vs Inheritance
- [ ] Tôi biết khi nào dùng Children prop
- [ ] Tôi biết khi nào dùng Named Slots (props)
- [ ] Tôi hiểu Compound Components pattern
- [ ] Tôi biết Container vs Presentational separation
- [ ] Tôi hiểu Render Props (legacy pattern)
- [ ] Tôi biết HOC là gì (legacy)
- [ ] Tôi biết tránh Props Drilling
- [ ] Tôi có thể chọn pattern phù hợp cho từng use case
- [ ] Tôi hiểu trade-offs của mỗi pattern
Code Review Checklist
Khi design components:
Composition:
- [ ] Favor composition over inheritance
- [ ] Components nhận children khi nội dung dynamic
- [ ] Dùng named props cho structured layouts
- [ ] Consider compound components cho complex UIs
Props:
- [ ] Không props drilling quá 2-3 levels
- [ ] Dùng composition để avoid drilling
- [ ] Props có meaningful names
- [ ] Optional props có default values
Structure:
- [ ] Tách Container (logic) và Presentational (UI)
- [ ] Components single responsibility
- [ ] Reusable và flexible
- [ ] Easy to test và maintain
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
1. Dropdown Menu Component
Tạo Dropdown với compound components:
- Dropdown container
- Dropdown.Trigger (button để open/close)
- Dropdown.Menu (danh sách options)
- Dropdown.Item (mỗi option)
// Expected usage:
<Dropdown>
<Dropdown.Trigger>
<button>Options ▼</button>
</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Item>Edit</Dropdown.Item>
<Dropdown.Item>Delete</Dropdown.Item>
<Dropdown.Item>Share</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>💡 Solution
/**
* Dropdown - Compound Component với menu thả xuống
* Sử dụng let để mô phỏng "state" (chỉ để demo và tự thay đổi giá trị thủ công)
* @param {ReactNode} children - Dropdown.Trigger, Dropdown.Menu, Dropdown.Item
*/
function Dropdown({ children }) {
// Mô phỏng state bằng biến let
// Bạn có thể tự thay đổi giá trị true/false ở đây để xem UI thay đổi
let isOpen = false; // ← thay thành true để xem menu mở
// Trong thực tế, không thể thay đổi giá trị này bằng cách click
// vì React không biết giá trị đã thay đổi → không re-render
const toggle = () => {
isOpen = !isOpen;
console.log('isOpen bây giờ là:', isOpen);
// UI KHÔNG cập nhật dù console log thay đổi
// Đây chính là lý do cần useState
};
return (
<div className='dropdown'>
{/* Truyền toggle và isOpen xuống các phần tử con */}
{React.Children.map(children, (child) => {
if (child.type === Dropdown.Trigger) {
return React.cloneElement(child, { onToggle: toggle });
}
if (child.type === Dropdown.Menu) {
return React.cloneElement(child, { isOpen });
}
return child;
})}
</div>
);
}
/**
* Trigger - Nút mở/đóng dropdown
* @param {function} onToggle - Hàm toggle (chỉ log, không thực sự re-render)
*/
Dropdown.Trigger = function DropdownTrigger({ children, onToggle }) {
return (
<div
className='dropdown-trigger'
onClick={onToggle}
style={{
cursor: 'pointer',
padding: '8px 16px',
border: '1px solid #ccc',
borderRadius: '4px',
display: 'inline-block',
}}
>
{children || 'Tùy chọn'}
<span style={{ marginLeft: '8px' }}>▼</span>
</div>
);
};
/**
* Menu - Danh sách các mục (chỉ hiển thị khi isOpen = true)
* @param {boolean} isOpen - Trạng thái mô phỏng
* @param {ReactNode} children - Các Dropdown.Item
*/
Dropdown.Menu = function DropdownMenu({ isOpen, children }) {
if (!isOpen) return null;
return (
<div
className='dropdown-menu'
style={{
position: 'absolute',
background: 'white',
border: '1px solid #ccc',
borderRadius: '4px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
minWidth: '160px',
marginTop: '4px',
zIndex: 10,
}}
>
{children}
</div>
);
};
/**
* Item - Một mục trong menu
* @param {ReactNode} children - Nội dung mục
* @param {function} [onClick] - Hành động khi click
*/
Dropdown.Item = function DropdownItem({ children, onClick }) {
return (
<div
className='dropdown-item'
onClick={onClick}
style={{
padding: '8px 16px',
cursor: 'pointer',
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f0f0f0')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'white')}
>
{children}
</div>
);
};Ví dụ sử dụng (cách test bằng tay):
function App() {
const handleEdit = () => console.log('Chọn sửa');
const handleDelete = () => console.log('Chọn xóa');
return (
<div style={{ padding: '80px 20px', position: 'relative' }}>
<h3>Dropdown với let mô phỏng state</h3>
<p>
Mở code và thay đổi dòng "let isOpen = false" thành true → xem menu xuất
hiện
</p>
<Dropdown>
<Dropdown.Trigger>Chọn hành động</Dropdown.Trigger>
<Dropdown.Menu>
<Dropdown.Item onClick={handleEdit}>✏️ Sửa</Dropdown.Item>
<Dropdown.Item onClick={handleDelete}>🗑️ Xóa</Dropdown.Item>
<Dropdown.Item>─────────────────</Dropdown.Item>
<Dropdown.Item>🚪 Thoát</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>
);
}📚 Key Learnings:
- Compound Components Pattern - Parent và sub-components hoạt động như một unit
- Shared State - isOpen state được share giữa Trigger và Menu
- Flexible API - Developer tự control structure và content của dropdown
- Clear Hierarchy - Dropdown.Trigger, Dropdown.Menu, Dropdown.Item rõ ràng vai trò từng phần
2. Alert Component với Variants
Tạo Alert component với named slots và variants:
- Variants: success, error, warning, info
- Slots: icon, title, message, actions
- Close button optional
<Alert
variant='success'
icon={<span>✓</span>}
title='Success!'
message='Your changes have been saved.'
onClose={() => console.log('Closed')}
/>💡 Solution
/**
* Alert - Component thông báo với các variant khác nhau
* Sử dụng let để mô phỏng "state" cho việc đóng alert (chỉ để demo)
* Bạn có thể thay đổi giá trị let isVisible = true/false để xem alert ẩn/hiện
* @param {('success'|'error'|'warning'|'info')} [variant='info'] - Loại thông báo
* @param {ReactNode} [icon] - Icon tùy chọn
* @param {ReactNode} [title] - Tiêu đề
* @param {ReactNode} message - Nội dung chính
* @param {function} [onClose] - Callback khi đóng (ở đây chỉ log)
*/
function Alert({ variant = 'info', icon, title, message, onClose }) {
// Mô phỏng state bằng biến let
// Bạn tự thay đổi giá trị true/false ở đây để xem alert biến mất
let isVisible = true; // ← thay thành false để ẩn alert
const handleClose = () => {
isVisible = false;
console.log('Alert đã được đóng (nhưng UI không cập nhật)');
if (onClose) onClose();
// Lưu ý: thay đổi let không gây re-render → UI vẫn hiển thị như cũ
};
// Các style theo variant
const styles = {
success: { borderColor: '#4caf50', bg: '#e8f5e9', text: '#2e7d32' },
error: { borderColor: '#f44336', bg: '#ffebee', text: '#c62828' },
warning: { borderColor: '#ff9800', bg: '#fff3e0', text: '#e65100' },
info: { borderColor: '#2196f3', bg: '#e3f2fd', text: '#1565c0' },
};
const current = styles[variant] || styles.info;
if (!isVisible) return null;
return (
<div
style={{
border: `1px solid ${current.borderColor}`,
backgroundColor: current.bg,
color: current.text,
padding: '16px',
borderRadius: '6px',
margin: '16px 0',
position: 'relative',
maxWidth: '500px',
}}
>
{icon && (
<div style={{ fontSize: '24px', marginBottom: '8px' }}>{icon}</div>
)}
{title && (
<div
style={{
fontWeight: 'bold',
fontSize: '1.2rem',
marginBottom: '8px',
}}
>
{title}
</div>
)}
<div style={{ marginBottom: onClose ? '24px' : '0' }}>{message}</div>
{onClose && (
<button
onClick={handleClose}
style={{
position: 'absolute',
top: '12px',
right: '12px',
background: 'none',
border: 'none',
fontSize: '20px',
cursor: 'pointer',
color: 'inherit',
opacity: 0.7,
}}
aria-label='Đóng thông báo'
>
×
</button>
)}
</div>
);
}Ví dụ sử dụng:
function App() {
const handleCloseSuccess = () => console.log('Đã đóng alert success');
return (
<div style={{ padding: '40px', maxWidth: '600px', margin: '0 auto' }}>
<h3>Demo Alert - Thay đổi let isVisible để test</h3>
{/* Alert 1: Success */}
<Alert
variant='success'
icon={<span>✓</span>}
title='Thành công!'
message='Dữ liệu của bạn đã được lưu thành công.'
onClose={handleCloseSuccess}
/>
{/* Alert 2: Error - không có nút đóng */}
<Alert
variant='error'
icon={<span>!</span>}
title='Lỗi kết nối'
message='Không thể kết nối đến server. Vui lòng kiểm tra mạng.'
/>
{/* Alert 3: Warning - chỉ message */}
<Alert
variant='warning'
message='Bạn còn 10% dung lượng lưu trữ. Hãy xóa bớt file không cần thiết.'
/>
</div>
);
}Hướng dẫn test thủ công:
- Ban đầu
let isVisible = true→ thấy tất cả alert - Thay thành
let isVisible = false→ save → tất cả alert biến mất ngay - Click nút × → console log "Alert đã được đóng", nhưng UI không thay đổi
→ Đây chính là minh chứng rõ ràng rằng biếnletkhông thể điều khiển UI trong React
→ CầnuseStateđể thay đổi giá trị và React tự động re-render
Khi học useState, bạn sẽ thay phần này bằng:
const [isVisible, setIsVisible] = useState(true);
// và dùng setIsVisible(false) trong handleCloseNâng cao (60 phút)
3. Stepper Component
Tạo multi-step wizard với compound components:
- Stepper container (track current step)
- Stepper.Step (mỗi step)
- Visual progress indicator
- Next/Previous buttons
<Stepper currentStep={2}>
<Stepper.Step
number={1}
title='Account'
>
<p>Step 1 content...</p>
</Stepper.Step>
<Stepper.Step
number={2}
title='Profile'
>
<p>Step 2 content...</p>
</Stepper.Step>
<Stepper.Step
number={3}
title='Confirm'
>
<p>Step 3 content...</p>
</Stepper.Step>
</Stepper>💡 Solution
/**
* Stepper - Component wizard đa bước với progress bar
* @param {number} currentStep - Bước hiện tại (từ 1 trở lên)
* @param {ReactNode} children - Các <Stepper.Step> component
*/
function Stepper({ currentStep = 1, children }) {
// Mô phỏng state bằng let - bạn có thể thay đổi số này để test
// let currentStep = 2; // ← uncomment nếu muốn hard-code thay vì dùng prop
// Lấy tất cả Step và sắp xếp theo number
const steps = React.Children.toArray(children)
.filter((child) => child.type === Stepper.Step)
.sort((a, b) => (a.props.number || 0) - (b.props.number || 0));
const totalSteps = steps.length;
// Tìm step đang active dựa trên number
const activeStep = steps.find((step) => step.props.number === currentStep);
return (
<div
style={{
maxWidth: '800px',
margin: '40px auto',
padding: '24px',
border: '1px solid #ddd',
borderRadius: '12px',
background: '#fff',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
}}
>
{/* Progress Bar */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
position: 'relative',
marginBottom: '48px',
}}
>
{/* Thanh nền nối các bước */}
<div
style={{
position: 'absolute',
top: '22px',
left: '24px',
right: '24px',
height: '4px',
background: '#e0e0e0',
zIndex: 1,
}}
/>
{/* Các bước (vòng tròn) */}
{steps.map((step) => {
const stepNum = step.props.number;
const isActive = stepNum === currentStep;
const isCompleted = stepNum < currentStep;
return (
<div
key={stepNum}
style={{
width: '52px',
height: '52px',
borderRadius: '50%',
background: isCompleted
? '#28a745'
: isActive
? '#007bff'
: '#e9ecef',
color: isCompleted || isActive ? '#fff' : '#6c757d',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
fontWeight: 'bold',
position: 'relative',
zIndex: 2,
boxShadow: isActive ? '0 0 0 4px rgba(0,123,255,0.25)' : 'none',
transition: 'all 0.3s',
}}
>
{isCompleted ? '✓' : stepNum}
</div>
);
})}
</div>
{/* Tiêu đề các bước */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: '32px',
padding: '0 24px',
}}
>
{steps.map((step) => {
const stepNum = step.props.number;
const isActive = stepNum === currentStep;
return (
<div
key={stepNum}
style={{
flex: 1,
textAlign: 'center',
fontSize: '15px',
fontWeight: isActive ? '600' : '500',
color: isActive
? '#007bff'
: stepNum < currentStep
? '#28a745'
: '#6c757d',
}}
>
{step.props.title || `Step ${stepNum}`}
</div>
);
})}
</div>
{/* Nội dung bước hiện tại */}
<div
style={{
padding: '24px',
background: '#f8f9fa',
borderRadius: '8px',
minHeight: '180px',
}}
>
{activeStep ? (
activeStep
) : (
<p style={{ color: '#dc3545', textAlign: 'center' }}>
Không tìm thấy bước có number = {currentStep}
<br />
Các bước hiện có:{' '}
{steps.map((s) => s.props.number).join(', ') || 'không có'}
</p>
)}
</div>
{/* Hướng dẫn test */}
<div
style={{
marginTop: '24px',
fontSize: '13px',
color: '#6c757d',
textAlign: 'center',
}}
>
<strong>Demo mode:</strong> Thay đổi prop <code>currentStep</code> trên
<Stepper> để xem các bước khác nhau
</div>
</div>
);
}
/**
* Step - Một bước trong Stepper
* @param {number} number - Số thứ tự bước (bắt buộc, dùng để khớp với currentStep)
* @param {string} title - Tiêu đề hiển thị trên progress bar
* @param {ReactNode} children - Nội dung của bước
*/
Stepper.Step = function Step({ number, title, children }) {
return <div data-step={number}>{children}</div>;
};Ví dụ sử dụng (đúng như yêu cầu trong bài tập):
<Stepper currentStep={2}>
<Stepper.Step
number={1}
title='Account'
>
<p>Step 1 content...</p>
<p>Nhập thông tin tài khoản của bạn</p>
</Stepper.Step>
<Stepper.Step
number={2}
title='Profile'
>
<p>Step 2 content...</p>
<p>Cập nhật hồ sơ cá nhân</p>
</Stepper.Step>
<Stepper.Step
number={3}
title='Confirm'
>
<p>Step 3 content...</p>
<p>Xác nhận thông tin trước khi hoàn tất</p>
</Stepper.Step>
</Stepper>Kết quả khi test:
currentStep={1}→ hiển thị bước Account (number=1), vòng đầu màu xanh dươngcurrentStep={2}→ hiển thị bước Profile (number=2), bước 1 hoàn thành (checkmark xanh lá)currentStep={3}→ hiển thị bước Confirm (number=3), hai bước trước hoàn thành
4. Split Pane Layout
Tạo resizable split pane:
- SplitPane container
- SplitPane.Left (left panel)
- SplitPane.Right (right panel)
- Draggable divider giữa 2 panels
- Support vertical/horizontal split
Hint: Dùng composition + event handlers (không cần resize logic thật, chỉ log events)
<SplitPane direction="horizontal">
<SplitPane.Left>Left Children</SplitPane.Left>
<SplitPane.Right>Right Children</SplitPane.Right>
</SplitPane>
<SplitPane direction="vertical">
<SplitPane.Left>Top Children</SplitPane.Left>
<SplitPane.Right>Bottom Children</SplitPane.Right>
</SplitPane>💡 Solution
/**
* SplitPane - Component chia màn hình thành 2 phần (Left / Right)
* Sử dụng React.Children API để xử lý children một cách an toàn và linh hoạt
* @param {'horizontal' | 'vertical'} [direction='horizontal'] - Hướng chia
* @param {ReactNode} children - Các phần con: SplitPane.Left và SplitPane.Right
*/
function SplitPane({ direction = 'horizontal', children }) {
// Mô phỏng state bằng let - bạn thay đổi để test
let splitPosition = 50; // ← Thay số này (30, 60, 20...) để xem chia màn hình khác nhau
// Cách 1: Dùng React.Children.toArray() + kiểm tra type (cách hiện tại trong code cũ)
const parts = React.Children.toArray(children);
// Tìm Left và Right theo type (linh hoạt hơn, không phụ thuộc thứ tự)
let leftPart = null;
let rightPart = null;
React.Children.forEach(children, (child) => {
if (child.type === SplitPane.Left) {
leftPart = child;
}
if (child.type === SplitPane.Right) {
rightPart = child;
}
});
// Nếu không tìm thấy đủ 2 phần → báo lỗi
if (!leftPart || !rightPart) {
return (
<div style={{ color: 'red', padding: '20px', border: '2px solid red' }}>
SplitPane cần đúng 2 phần con: <strong>SplitPane.Left</strong> và{' '}
<strong>SplitPane.Right</strong>
<br />
<br />
Đã tìm thấy: {React.Children.count(children)} phần con
</div>
);
}
const isHorizontal = direction === 'horizontal';
return (
<div
style={{
display: 'flex',
flexDirection: isHorizontal ? 'row' : 'column',
height: '500px',
width: '100%',
border: '1px solid #ccc',
borderRadius: '8px',
overflow: 'hidden',
background: '#f8f9fa',
}}
>
{/* Phần Left (trái hoặc trên) */}
<div
style={{
flex: `${splitPosition} 1 0%`,
padding: '24px',
overflow: 'auto',
background: '#ffffff',
borderRight: isHorizontal ? '1px solid #ddd' : 'none',
borderBottom: isHorizontal ? 'none' : '1px solid #ddd',
}}
>
{leftPart}
</div>
{/* Thanh kéo (Divider) */}
<div
style={{
width: isHorizontal ? '10px' : '100%',
height: isHorizontal ? '100%' : '10px',
background: '#ced4da',
cursor: isHorizontal ? 'col-resize' : 'row-resize',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background 0.2s',
}}
title='Thay splitPosition trong code để thay đổi kích thước'
>
<div
style={{
width: isHorizontal ? '4px' : '80px',
height: isHorizontal ? '80px' : '4px',
background: '#6c757d',
borderRadius: '4px',
}}
/>
</div>
{/* Phần Right (phải hoặc dưới) */}
<div
style={{
flex: `${100 - splitPosition} 1 0%`,
padding: '24px',
overflow: 'auto',
background: '#f1f3f5',
}}
>
{rightPart}
</div>
</div>
);
}
/**
* Left - Phần bên trái hoặc phần trên (vertical)
* @param {ReactNode} children - Nội dung
*/
SplitPane.Left = function SplitPaneLeft({ children }) {
return <>{children}</>;
};
/**
* Right - Phần bên phải hoặc phần dưới (vertical)
* @param {ReactNode} children - Nội dung
*/
SplitPane.Right = function SplitPaneRight({ children }) {
return <>{children}</>;
};Ví dụ sử dụng (thứ tự Left/Right không quan trọng)
function App() {
return (
<div style={{ padding: '40px' }}>
<h2>SplitPane - Thứ tự Left/Right linh hoạt</h2>
{/* Ví dụ 1: Horizontal - Left trước Right */}
<SplitPane direction='horizontal'>
<SplitPane.Left>
<h3>Editor (Left)</h3>
<p>Viết code ở đây...</p>
<pre
style={{
background: '#f4f4f4',
padding: '12px',
borderRadius: '6px',
}}
>
console.log("Hello SplitPane");
</pre>
</SplitPane.Left>
<SplitPane.Right>
<h3>Preview (Right)</h3>
<div
style={{
background: '#fff',
padding: '16px',
border: '1px solid #ddd',
}}
>
Kết quả render ở đây
</div>
</SplitPane.Right>
</SplitPane>
{/* Ví dụ 2: Vertical - Right trước Left (vẫn hoạt động) */}
<div style={{ marginTop: '60px' }}>
<SplitPane direction='vertical'>
<SplitPane.Right>
<h3>Phần Dưới (Right)</h3>
<p>Danh sách log hoặc kết quả</p>
</SplitPane.Right>
<SplitPane.Left>
<h3>Phần Trên (Left)</h3>
<p>Bản đồ hoặc hình ảnh lớn</p>
</SplitPane.Left>
</SplitPane>
</div>
<p style={{ marginTop: '40px', color: '#666' }}>
<strong>Demo:</strong> Thay <code>let splitPosition = 50;</code> thành
30, 70... để xem panel thay đổi kích thước
</p>
</div>
);
}Giải thích các React.Children API được sử dụng:
React.Children.toArray(children): Chuyển children thành array thật để dễ thao tácReact.Children.forEach(children, fn): Duyệt qua từng child để kiểm tra type và gán vào biến (an toàn hơn map khi chỉ cần side-effect)React.Children.count(children): Đếm số lượng children (dùng để báo lỗi nếu thiếu)- Không dùng
React.Children.mapở đây vì ta không cần bọc lại child, chỉ cần tìm và tham chiếu
Ưu điểm của cách này:
- Không phụ thuộc thứ tự Left/Right trong markup
- Báo lỗi rõ ràng nếu thiếu một trong hai phần
- Dễ mở rộng sau này (ví dụ: thêm prop
minSize,maxSize,...)
Khi học useState + event handling, bạn sẽ thêm logic kéo chuột thật sự để thay đổi splitPosition động.
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
React Docs - Composition vs Inheritancehttps://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children
Compound Components Patternhttps://kentcdodds.com/blog/compound-components-with-react-hooks
Đọc thêm
Container/Presentational Patternhttps://www.patterns.dev/posts/presentational-container-pattern
React Component Patternshttps://www.robinwieruch.de/react-component-composition/
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền (Đã học)
- Ngày 4: Props & Children (nền tảng cho composition)
- Ngày 5: Events (pass handlers through composition)
- Ngày 6: Lists (render composed components)
Hướng tới (Sẽ học)
- Ngày 11: useState (shared state trong compound components)
- Ngày 24: Custom Hooks (alternative cho Render Props/HOC)
- Ngày 36: Context API (solve Props Drilling)
- Ngày 39: Advanced Patterns (composition patterns nâng cao)
💡 SENIOR INSIGHTS
Cân Nhắc Production
1. Component API Design:
// ❌ BAD: Too many props
<Button
text="Click"
icon="check"
iconPosition="left"
variant="primary"
size="large"
loading={false}
disabled={false}
onClick={...}
// ... 20 more props
/>
// ✅ GOOD: Composition
<Button variant="primary" size="large" onClick={...}>
<Icon name="check" />
Click Me
</Button>2. Over-engineering Warning:
// ❌ Over-abstracted
<SuperFlexibleComponent
renderHeader={(props) => ...}
renderBody={(props) => ...}
headerProps={{...}}
bodyProps={{...}}
// Too complex!
/>
// ✅ KISS Principle
<Card>
<h3>Title</h3>
<p>Content</p>
</Card>3. Accessibility:
// ✅ Compound components với a11y
<Tabs>
<Tabs.List role='tablist'>
<Tabs.Tab
role='tab'
aria-selected={true}
>
Tab 1
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel role='tabpanel'>Content</Tabs.Panel>
</Tabs>Câu Hỏi Phỏng Vấn
Junior Level:
Q1: "Composition và Inheritance khác nhau như thế nào trong React?"
A: React khuyến khích composition (lắp ghép components) thay vì inheritance (kế thừa classes). Composition linh hoạt hơn, dễ reuse và maintain hơn.
Q2: "Children prop là gì?"
A: Children là prop đặc biệt chứa nội dung bên trong component tags. Giúp component linh hoạt, có thể nhận bất kỳ content nào.
Mid Level:
Q3: "Compound Components là gì? Cho ví dụ."
A: Pattern mà nhiều components hoạt động cùng nhau như một unit. VD: <Select>, <Select.Option>. Lợi ích: API rõ ràng, shared state, flexible structure.
Q4: "Làm sao tránh Props Drilling?"
A:
- Composition (ví dụ: đưa component con vào trực tiếp component cần data thay vì truyền qua nhiều tầng) - di chuyển component lại gần nơi sử dụng data
- Context API (ví dụ: AuthContext, ThemeContext) - chia sẻ data ở phạm vi toàn cục
- State management libraries (Redux, Zustand) (ví dụ: lưu user, cart ở store chung)
- Component specialization (ví dụ: tạo tự lấy user từ context/store thay vì nhận nhiều props từ component cha)
Senior Level:
Q5: "Trade-offs giữa Compound Components và Render Props?" A:
- Compound: API rõ ràng, dễ sử dụng, DX tốt hơn (DX = Developer Experience: dễ đọc, dễ hiểu, autocomplete tốt, ít bug khi dùng). Nhưng cần shared state (Context).
- Render Props: Linh hoạt tối đa, không cần setup. Nhưng syntax phức tạp, khó đọc, DX kém hơn (nhiều function lồng nhau, khó maintain).
- Modern: Ưu tiên Custom Hooks để tái sử dụng logic.
War Stories
Story 1: The Props Drilling Nightmare
"Refactor một form lớn: user prop drill qua 8 levels! Thêm 1 field mới = sửa 15 files. Team quyết định: restructure với composition. Di chuyển UserContext lên top, các components con dùng Context. Refactor 2 ngày nhưng maintain sau đó EZ. Lesson: Detect props drilling sớm!"
Story 2: Over-abstraction Hell
"Junior dev tạo 'universal component' - component siêu cấp vũ trụ, có thể làm MỌI THỨ: 50+ props, 10+ render props, config object khổng lồ. Không ai hiểu cách dùng! Code review: Break down thành smaller, specific components. Lesson: KISS > Clever abstractions!"
Story 3: The Compound Components Win
"Thiết kế lại Table component: ban đầu dùng một props object khổng lồ. Chuyển sang compound pattern:
<Table><Table.Header /><Table.Row /></Table>. Code dễ đọc hơn gấp 10 lần, custom dễ hơn, bugs giảm. Team lead: 'This is the way!' Bài học: Chọn đúng pattern = DX tốt hơn!"
🎯 PREVIEW NGÀY MAI
Ngày 8: Mini Project - Static Product Catalog
Tomorrow chúng ta sẽ:
- Tổng hợp kiến thức Ngày 1-7
- Build một project thực tế
- Không có concepts mới
- Focus vào best practices
- Component architecture
- Code organization
Chuẩn bị:
- Review tất cả concepts đã học (JSX → Composition)
- Nghĩ về structure của một e-commerce product page
- Ready để code một project hoàn chỉnh!
Sneak peek:
Project: Product Catalog
├── ProductGrid (container)
├── ProductCard (item)
│ ├── ProductImage
│ ├── ProductInfo
│ └── ProductActions
├── FilterBar (sidebar)
└── Layout (composition)🎊 CHÚC MỪNG!
Bạn đã hoàn thành Ngày 7: Component Composition!
Hôm nay bạn đã học: ✅ Composition vs Inheritance philosophy
✅ Children prop patterns (basic → advanced)
✅ Named Slots (props) cho structured layouts
✅ Compound Components cho complex UIs
✅ Container/Presentational separation
✅ Render Props & HOC (legacy patterns)
✅ Real-world composition patterns
Key Takeaways:
- Composition > Inheritance trong React
- Children prop cho flexibility
- Compound Components cho clear APIs
- Avoid Props Drilling bằng composition
- KISS Principle - đừng over-abstract
Next steps:
- Hoàn thành bài tập về nhà
- Practice tạo compound components
- Refactor old code dùng composition
- Chuẩn bị cho Ngày 8: Mini Project!
Remember: Good component design = Reusable + Flexible + Maintainable! 🎯
Keep coding! 💪 Tomorrow: Hands-on Project Time! 🚀