Skip to content

📅 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:

  1. Children prop là gì? Cho ví dụ cách dùng.
  2. Props có thể truyền những gì? (string, number, function...?)
  3. Component có thể return component khác không?
💡 Xem đáp án
  1. Children prop:
jsx
// 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>;
}
  1. Props có thể là:
jsx
<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
/>
  1. Component return component:
jsx
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:

jsx
// 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):

jsx
// ❌ 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 coupling

Cách React (Composition):

jsx
// ✅ 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:

  1. Components như LEGO blocks - lắp ghép linh hoạt
  2. Behavior qua Props - không phải inheritance
  3. Reuse qua Composition - không phải class hierarchy

1.3 Mental Model: Containment (Chứa đựng)

jsx
// 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"

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"

jsx
// ❌ 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"

jsx
// ❌ 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 ⭐

jsx
// Pattern 1: Simple Wrapper
function Container({ children }) {
  return <div className='container'>{children}</div>;
}

// Usage
<Container>
  <h1>Welcome</h1>
  <p>This is content</p>
</Container>;
jsx
// 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>;
jsx
// 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) ⭐⭐

jsx
// 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

jsx
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>&copy; 2025 Company</p>}
/>;

Demo 3: Compound Components ⭐⭐⭐

Pattern mạnh nhất cho complex UIs!

jsx
// 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

jsx
// 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)

jsx
// ❌ 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
jsx
// ✅ 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 TypeResponsibilityExample
ContainerLogic, data, stateUserProfileContainer
PresentationalUI, stylingUserProfileView

Demo 6: Higher-Order Components (HOC) ⭐⭐

Pattern: Function nhận Component, return Enhanced Component

jsx
// 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

jsx
// 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 )

jsx
// 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>;
jsx
// 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"
jsx
// 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

jsx
// 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:

MethodMục đíchVí 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 childrenChỉ dùng cho side effects
React.Children.count(children)Đếm số childrenLogic điều kiện
React.Children.only(children)Đảm bảo chỉ có 1 childComponent wrapper
React.Children.toArray(children)Chuyển thành arrayXử 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):

jsx
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ớ:

  1. React.Children.map an toàn hơn children.map() (xử lý được trường hợp chỉ có 1 child)
  2. React.cloneElement dùng để inject props vào children
  3. Pattern phổ biến trong Compound Components
  4. 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:

  1. Card component nhận: image, title, description, actions
  2. Tất cả slots đều optional
  3. Nếu không có image → không render image section
  4. Nếu không có actions → không render footer
jsx
/**
 * 💡 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
jsx
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:

  1. Accordion container
  2. Accordion.Item - mỗi item độc lập
  3. Accordion.Header - clickable để toggle
  4. Accordion.Content - nội dung expand/collapse
  5. Click header → toggle content visibility
jsx
// 🎯 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
jsx
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
jsx
// 🎯 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
jsx
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:

  1. Backdrop click → close modal
  2. Esc key → close modal
  3. Click content → không close (stopPropagation)
  4. Flexible composition → có thể skip header/footer
  5. 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:

  1. Layout Structure:

    • Option A: Flat structure với positioning
    • Option B: Nested structure với composition
    • Decision: ?
  2. Sidebar Behavior:

    • Always visible?
    • Collapsible?
    • Responsive (hide on mobile)?

ADR:

markdown
## 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)

jsx
// 🎯 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>&copy; 2025 Company Name</p>
      </AppLayout.Footer>
    </AppLayout>
  );
}
💡 Solution
jsx
// 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>&copy; 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:

  1. Flexible composition - bất kỳ layout nào
  2. Clear structure - dễ đọc, dễ maintain
  3. Reusable - dùng cho nhiều pages
  4. Responsive - dễ thêm responsive logic sau
  5. 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
jsx
// 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)
jsx
// 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:

  1. Theme object - centralized design tokens
  2. Provider pattern - wrap app to provide theme
  3. Render props - pass theme to children via function
  4. Inline styles - dynamic styling based on theme
  5. 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

PatternUse CasePros ✅Cons ❌Example
Children PropSimple wrappers• Simple
• Flexible content
• No structure
• Hard to target specific parts
<Card>{content}</Card>
Named SlotsStructured layouts• Clear structure
• Easy to style parts
• More props
• Verbose
<Card header={...} body={...} />
Compound ComponentsComplex UIs• Flexible API
• Shared state
• Clear hierarchy
• More complex
• Learning curve
<Tabs><Tabs.Tab /></Tabs>
Render PropsLogic reuse• Maximum flexibility
• Access to internals
• Complex syntax
• Callback hell
<Mouse render={({x,y}) => ...} />
HOCCross-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 ❌

jsx
// 🐛 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:

  1. Vấn đề gì với code này?
  2. Nếu thêm prop mới, phải sửa ở đâu?
  3. 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 user như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:

jsx
// 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)

jsx
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)

jsx
// 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

jsx
// 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 ❌

jsx
// 🐛 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:

  1. Tại sao children không hiển thị?
  2. Làm sao fix?
💡 Giải đáp

1. Tại sao:

  • Component nhận children prop nhưng không render nó!
  • Hard-coded content <p>Fixed content here</p> thay thế children

2. Fix:

jsx
// ✅ 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 ❌

jsx
// 🐛 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:

  1. Vấn đề gì?
  2. Tab và Panel cần gì để "communicate"?
  3. 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):

jsx
// 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):

jsx
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)
jsx
// 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
jsx
/**
 * 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):

jsx
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:

  1. Compound Components Pattern - Parent và sub-components hoạt động như một unit
  2. Shared State - isOpen state được share giữa Trigger và Menu
  3. Flexible API - Developer tự control structure và content của dropdown
  4. 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
jsx
<Alert
  variant='success'
  icon={<span>✓</span>}
  title='Success!'
  message='Your changes have been saved.'
  onClose={() => console.log('Closed')}
/>
💡 Solution
jsx
/**
 * 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:

jsx
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:

  1. Ban đầu let isVisible = true → thấy tất cả alert
  2. Thay thành let isVisible = false → save → tất cả alert biến mất ngay
  3. 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ến let không thể điều khiển UI trong React
    → Cần useState để thay đổi giá trị và React tự động re-render

Khi học useState, bạn sẽ thay phần này bằng:

jsx
const [isVisible, setIsVisible] = useState(true);
// và dùng setIsVisible(false) trong handleClose

Nâ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
jsx
<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
jsx
/**
 * 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
        &lt;Stepper&gt; để 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):

jsx
<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ương
  • currentStep={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)

jsx
<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
jsx
/**
 * 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)

jsx
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ác
  • React.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

  1. React Docs - Composition vs Inheritancehttps://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children

  2. Compound Components Patternhttps://kentcdodds.com/blog/compound-components-with-react-hooks

Đọc thêm

  1. Container/Presentational Patternhttps://www.patterns.dev/posts/presentational-container-pattern

  2. 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:

jsx
// ❌ 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:

jsx
// ❌ Over-abstracted
<SuperFlexibleComponent
  renderHeader={(props) => ...}
  renderBody={(props) => ...}
  headerProps={{...}}
  bodyProps={{...}}
  // Too complex!
/>

// ✅ KISS Principle
<Card>
  <h3>Title</h3>
  <p>Content</p>
</Card>

3. Accessibility:

jsx
// ✅ 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:

  1. 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
  2. Context API (ví dụ: AuthContext, ThemeContext) - chia sẻ data ở phạm vi toàn cục
  3. State management libraries (Redux, Zustand) (ví dụ: lưu user, cart ở store chung)
  4. 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:

  1. Hoàn thành bài tập về nhà
  2. Practice tạo compound components
  3. Refactor old code dùng composition
  4. Chuẩn bị cho Ngày 8: Mini Project!

Remember: Good component design = Reusable + Flexible + Maintainable! 🎯

Keep coding! 💪 Tomorrow: Hands-on Project Time! 🚀

Personal tech knowledge base