Skip to content

📅 NGÀY 4: COMPONENTS & PROPS - NỀN TẢNG GIAO TIẾP TRONG REACT

🎯 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õ Function Components là gì và cách tạo components
  • [ ] Nắm vững Props flow - dữ liệu chảy từ parent xuống child như thế nào
  • [ ] Thành thạo Props destructuring để code clean và readable hơn
  • [ ] Sử dụng được Children prop cho component composition
  • [ ] Phân biệt được khi nào nên tách component, khi nào không

🤔 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. JSX là gì? Nó khác gì với HTML?
  2. JavaScript expression trong JSX được viết như thế nào? (Hint: dùng dấu gì?)
  3. Array method .map() dùng để làm gì? Cho ví dụ đơn giản.
💡 Solution
  1. JSX là syntax extension của JavaScript, cho phép viết UI markup giống HTML nhưng có thể embed JavaScript expressions. Khác HTML ở chỗ: className thay vì class, htmlFor thay vì for, camelCase cho attributes.

  2. Dùng dấu {}: <div>{userName}</div>, <img src={imageUrl} />

  3. .map() tạo array mới bằng cách transform từng phần tử:

javascript
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2); // [2, 4, 6]

📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)

1.1 Vấn Đề Thực Tế

Hãy tưởng tượng bạn đang xây dựng Facebook. Bạn cần hiển thị 1000 bài posts. Nếu viết code như này:

jsx
function App() {
  return (
    <div>
      {/* Post 1 */}
      <div className='post'>
        <img src='avatar1.jpg' />
        <h3>John Doe</h3>
        <p>This is my first post!</p>
        <button>Like</button>
      </div>

      {/* Post 2 */}
      <div className='post'>
        <img src='avatar2.jpg' />
        <h3>Jane Smith</h3>
        <p>Hello world!</p>
        <button>Like</button>
      </div>

      {/* ... 998 posts nữa??? 😱 */}
    </div>
  );
}

Vấn đề:

  • 🔴 Lặp code 1000 lần
  • 🔴 Thay đổi design 1 post = sửa 1000 chỗ
  • 🔴 Không thể tái sử dụng
  • 🔴 Code dài vô tận, không maintain được

1.2 Giải Pháp: Components & Props

Components giống như khuôn bánh - bạn tạo 1 khuôn, sau đó đổ nguyên liệu khác nhau vào để ra bánh khác nhau.

Props (viết tắt của Properties) là nguyên liệu - dữ liệu bạn truyền vào component để tạo output khác nhau.

jsx
// Component = Khuôn bánh
function Post(props) {
  return (
    <div className='post'>
      <img src={props.avatar} />
      <h3>{props.author}</h3>
      <p>{props.content}</p>
      <button>Like</button>
    </div>
  );
}

// Sử dụng - Đổ nguyên liệu vào khuôn
function App() {
  return (
    <div>
      <Post
        avatar='avatar1.jpg'
        author='John Doe'
        content='This is my first post!'
      />
      <Post
        avatar='avatar2.jpg'
        author='Jane Smith'
        content='Hello world!'
      />
    </div>
  );
}

Lợi ích:

  • ✅ Viết 1 lần, dùng lại nhiều lần
  • ✅ Thay đổi design ở 1 chỗ
  • ✅ Code ngắn gọn, dễ đọc
  • ✅ Dễ test và maintain

1.3 Mental Model

┌─────────────────────────────────────────┐
│          PARENT COMPONENT               │
│  ┌───────────────────────────────┐      │
│  │   <Post                       │      │
│  │     avatar="john.jpg"   ──────┼──┐   │
│  │     author="John"       ──────┼──┤   │
│  │     content="Hello"     ──────┼──┤   │
│  │   />                          │  │   │
│  └───────────────────────────────┘  │   │
└─────────────────────────────────────┼───┘

                   PROPS FLOW         │
                   (One-way down)     │

┌─────────────────────────────────────────┐
│          CHILD COMPONENT                │
│  ┌───────────────────────────────┐      │
│  │   function Post(props) {      │      │
│  │     // props = {              │      │
│  │     //   avatar: "john.jpg",  │      │
│  │     //   author: "John",      │      │
│  │     //   content: "Hello"     │      │
│  │     // }                      │      │
│  │   }                           │      │
│  └───────────────────────────────┘      │
└─────────────────────────────────────────┘

🔑 Nguyên tắc vàng:

  1. Props chảy một chiều (one-way data flow) từ parent → child
  2. Props là read-only (immutable) - child KHÔNG được sửa props
  3. Mỗi lần props thay đổi → component re-render

1.4 Hiểu Lầm Phổ Biến

Hiểu lầm 1: "Props giống variables, tôi có thể sửa được"

jsx
function Post(props) {
  props.author = 'Changed'; // ❌ KHÔNG BAO GIỜ LÀM THẾ NÀY!
  return <h3>{props.author}</h3>;
}

Tại sao sai: Props là read-only. React sẽ warning và behavior không đoán trước được.


Hiểu lầm 2: "Tôi có thể gửi props từ child lên parent"

jsx
// ❌ SAI: Child không thể "push" data lên parent trực tiếp
function Child() {
  const data = 'Hello';
  // Làm sao gửi 'data' lên Parent??? - KHÔNG THỂ!
}

function Parent() {
  return <Child />;
}

Giải pháp đúng: Dùng callback functions (sẽ học sau).


Hiểu lầm 3: "Component tên phải viết thường"

jsx
function post(props) {
  // ❌ SAI - tên viết thường
  return <div>{props.title}</div>;
}

// React nghĩ 'post' là HTML tag, không phải component!
<post title='Hello' />; // ❌ Không hoạt động

✅ Đúng: Component tên phải viết HOA chữ cái đầu (PascalCase)

jsx
function Post(props) {
  // ✅ ĐÚNG
  return <div>{props.title}</div>;
}

💻 PHẦN 2: LIVE CODING (45 phút)

Demo 1: Pattern Cơ Bản ⭐

Use case: Tạo component Card hiển thị thông tin sản phẩm

jsx
// ❌ CÁCH SAI: Hard-code data trong component
function ProductCard() {
  return (
    <div className='card'>
      <img
        src='phone.jpg'
        alt='Product'
      />
      <h3>iPhone 15</h3>
      <p>$999</p>
    </div>
  );
}

// Vấn đề: Làm sao hiển thị laptop, tablet???
// Phải tạo ProductCard2, ProductCard3??? ❌
jsx
// ✅ CÁCH ĐÚNG: Dùng props để customize
function ProductCard(props) {
  return (
    <div className='card'>
      <img
        src={props.image}
        alt={props.name}
      />
      <h3>{props.name}</h3>
      <p>${props.price}</p>
    </div>
  );
}

// Sử dụng - Flexible & Reusable
function App() {
  return (
    <div>
      <ProductCard
        image='phone.jpg'
        name='iPhone 15'
        price={999}
      />
      <ProductCard
        image='laptop.jpg'
        name='MacBook Pro'
        price={2499}
      />
      <ProductCard
        image='tablet.jpg'
        name='iPad Air'
        price={599}
      />
    </div>
  );
}

🎯 Học được gì:

  • Props giúp component linh hoạt, tái sử dụng
  • 1 component definition → nhiều instances khác nhau
  • Props có thể là string, number, boolean, object, array, function...

Demo 2: Props Destructuring ⭐⭐

Vấn đề: Viết props. nhiều lần rất dài dòng

jsx
// ❌ CÁCH 1: Lặp đi lặp lại props.
function UserProfile(props) {
  return (
    <div>
      <img
        src={props.avatar}
        alt={props.name}
      />
      <h2>{props.name}</h2>
      <p>{props.bio}</p>
      <span>{props.followers} followers</span>
    </div>
  );
}
// Đọc: props. props. props. → Annoying! 😤
jsx
// ✅ CÁCH 2: Destructuring trong function body
function UserProfile(props) {
  const { avatar, name, bio, followers } = props;

  return (
    <div>
      <img
        src={avatar}
        alt={name}
      />
      <h2>{name}</h2>
      <p>{bio}</p>
      <span>{followers} followers</span>
    </div>
  );
}
// Tốt hơn, nhưng vẫn có thể ngắn gọn hơn
jsx
// ✅✅ CÁCH 3: Destructuring trực tiếp trong params (BEST PRACTICE)
function UserProfile({ avatar, name, bio, followers }) {
  return (
    <div>
      <img
        src={avatar}
        alt={name}
      />
      <h2>{name}</h2>
      <p>{bio}</p>
      <span>{followers} followers</span>
    </div>
  );
}
// Clean, concise, professional! ✨

🔥 Advanced: Default values

jsx
function UserProfile({
  avatar = 'default-avatar.png', // Default nếu không truyền
  name = 'Anonymous',
  bio = 'No bio available',
  followers = 0,
}) {
  return (
    <div>
      <img
        src={avatar}
        alt={name}
      />
      <h2>{name}</h2>
      <p>{bio}</p>
      <span>{followers} followers</span>
    </div>
  );
}

// Usage
<UserProfile name='Alice' />;
// avatar, bio, followers dùng default values

⚠️ LƯU Ý: Vẫn có thể access props object nếu cần

jsx
function Component({ name, ...restProps }) {
  console.log(restProps); // Tất cả props khác ngoài 'name'

  return <div>{name}</div>;
}

Demo 3: Children Prop ⭐⭐⭐

Children prop là prop đặc biệt - nội dung bên trong component tags.

jsx
// ❌ CÁCH SAI: Hardcode content trong component
function Card() {
  return (
    <div className='card'>
      <h3>Fixed Title</h3>
      <p>Fixed content that can't change</p>
    </div>
  );
}

// Vấn đề: Làm sao thay đổi content??? ❌
jsx
// ✅ CÁCH ĐÚNG: Dùng children prop
function Card({ children }) {
  return <div className='card'>{children}</div>;
}

// Usage - Flexible content!
function App() {
  return (
    <div>
      <Card>
        <h3>Product 1</h3>
        <p>This is a phone</p>
      </Card>

      <Card>
        <img src='banner.jpg' />
        <button>Click me</button>
      </Card>

      <Card>
        <ul>
          <li>Item 1</li>
          <li>Item 2</li>
        </ul>
      </Card>
    </div>
  );
}

🎯 Khi nào dùng children:

  • ✅ Component wrapper/container (Modal, Card, Layout)
  • ✅ Content có thể thay đổi tùy context
  • ✅ Muốn component flexible, không biết trước content

🎯 Khi nào dùng props thông thường:

  • ✅ Dữ liệu cụ thể (title, price, name...)
  • ✅ Cần validate/transform data
  • ✅ Data là primitive types (string, number)

Real-world Example: Modal Component

jsx
function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return (
    <div
      className='modal-overlay'
      onClick={onClose}
    >
      <div
        className='modal-content'
        onClick={(e) => e.stopPropagation()}
      >
        <button
          className='close-btn'
          onClick={onClose}
        >
          ×
        </button>
        {children}
      </div>
    </div>
  );
}

// Usage - Content thay đổi tùy ngữ cảnh
function App() {
  return (
    <>
      <Modal
        isOpen={true}
        onClose={() => {}}
      >
        <h2>Login</h2>
        <input
          type='text'
          placeholder='Username'
        />
        <button>Submit</button>
      </Modal>

      <Modal
        isOpen={true}
        onClose={() => {}}
      >
        <h2>Confirm Delete</h2>
        <p>Are you sure?</p>
        <button>Yes</button>
        <button>No</button>
      </Modal>
    </>
  );
}

🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)

⭐ Exercise 1: Button Component (15 phút)

🎯 Mục tiêu: Tạo reusable Button component với props
⏱️ Thời gian: 15 phút
🚫 KHÔNG dùng: State, Events (chỉ log ra console)

Requirements:

  1. Tạo Button component nhận props: text, color, size
  2. Support 3 sizes: small, medium, large
  3. Support 3 colors: primary, secondary, danger
  4. Khi click log ra console: "Button clicked: [text]"
jsx
/**
 * 💡 Gợi ý:
 * - Dùng template literals để tạo className dynamic
 * - className có thể là: `btn btn-${color} btn-${size}`
 */

// 🎯 NHIỆM VỤ CỦA BẠN:
function Button(/* TODO: Add props */) {
  return (
    <button
      className={/* TODO: Dynamic className */}
      onClick={() => console.log(/* TODO */)}
    >
      {/* TODO: Display text */}
    </button>
  );
}

// Test cases
function App() {
  return (
    <div>
      <Button text="Click Me" color="primary" size="small" />
      <Button text="Submit" color="secondary" size="medium" />
      <Button text="Delete" color="danger" size="large" />
    </div>
  );
}
💡 Solution
jsx
// Solution 1: Props object
function Button(props) {
  const className = `btn btn-${props.color} btn-${props.size}`;

  return (
    <button
      className={className}
      onClick={() => console.log(`Button clicked: ${props.text}`)}
    >
      {props.text}
    </button>
  );
}

// Solution 2: Destructuring (RECOMMENDED)
function Button({ text, color, size }) {
  return (
    <button
      className={`btn btn-${color} btn-${size}`}
      onClick={() => console.log(`Button clicked: ${text}`)}
    >
      {text}
    </button>
  );
}

// Solution 3: With default values
function Button({ text = 'Button', color = 'primary', size = 'medium' }) {
  return (
    <button
      className={`btn btn-${color} btn-${size}`}
      onClick={() => console.log(`Button clicked: ${text}`)}
    >
      {text}
    </button>
  );
}

CSS để test:

css
.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn-small {
  font-size: 12px;
}
.btn-medium {
  font-size: 14px;
}
.btn-large {
  font-size: 18px;
}

.btn-primary {
  background: #007bff;
  color: white;
}
.btn-secondary {
  background: #6c757d;
  color: white;
}
.btn-danger {
  background: #dc3545;
  color: white;
}

⭐⭐ Exercise 2: Comment Component (25 phút)

🎯 Mục tiêu: Tạo Comment component giống Facebook
⏱️ Thời gian: 25 phút

Scenario: Bạn đang xây dựng Facebook. Cần component hiển thị comments với avatar, tên, thời gian, và nội dung.

🤔 PHÂN TÍCH:

Approach A: Truyền nhiều props riêng lẻ

jsx
<Comment
  avatar='user1.jpg'
  userName='John Doe'
  timestamp='2 hours ago'
  content='Great post!'
/>
  • Pros: Rõ ràng, dễ hiểu
  • Cons: Nhiều props, dài dòng nếu mở rộng

Approach B: Truyền 1 object

jsx
<Comment
  data={{
    avatar: 'user1.jpg',
    userName: 'John Doe',
    timestamp: '2 hours ago',
    content: 'Great post!',
  }}
/>
  • Pros: Gọn gàng, dễ thêm fields
  • Cons: Phải access data.avatar, data.userName...

💭 BẠN CHỌN GÌ VÀ TẠI SAO?

Requirements:

  1. Hiển thị avatar (hình tròn)
  2. Tên user (bold)
  3. Thời gian (màu xám, nhỏ hơn)
  4. Nội dung comment
  5. Số likes (hiển thị icon ❤️ và số)
jsx
// 🎯 NHIỆM VỤ:
function Comment(/* TODO */) {
  return <div className='comment'>{/* TODO: Implement */}</div>;
}

// Test case
function App() {
  return (
    <div>
      <Comment
        avatar='https://i.pravatar.cc/150?img=1'
        userName='Alice Johnson'
        timestamp='5 minutes ago'
        content='This is amazing! 🎉'
        likes={12}
      />
      <Comment
        avatar='https://i.pravatar.cc/150?img=2'
        userName='Bob Smith'
        timestamp='1 hour ago'
        content='I totally agree with you!'
        likes={5}
      />
    </div>
  );
}
💡 Solution
jsx
// Solution: Destructuring approach
function Comment({ avatar, userName, timestamp, content, likes }) {
  return (
    <div className='comment'>
      <img
        src={avatar}
        alt={userName}
        className='comment-avatar'
      />
      <div className='comment-body'>
        <div className='comment-header'>
          <span className='comment-author'>{userName}</span>
          <span className='comment-timestamp'>{timestamp}</span>
        </div>
        <p className='comment-content'>{content}</p>
        <div className='comment-footer'>
          <span className='comment-likes'>❤️ {likes}</span>
        </div>
      </div>
    </div>
  );
}

CSS:

css
.comment {
  display: flex;
  gap: 12px;
  padding: 12px;
  border-bottom: 1px solid #eee;
}

.comment-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
}

.comment-body {
  flex: 1;
}

.comment-header {
  display: flex;
  gap: 8px;
  margin-bottom: 4px;
}

.comment-author {
  font-weight: bold;
}

.comment-timestamp {
  color: #666;
  font-size: 12px;
}

.comment-content {
  margin: 4px 0;
  line-height: 1.4;
}

.comment-likes {
  font-size: 12px;
  color: #666;
}

💡 Giải thích quyết định:

  • Chọn Approach A (props riêng lẻ) vì:
    • Dễ đọc, dễ hiểu
    • IDE autocomplete tốt hơn
    • Type safety tốt hơn (TypeScript)
    • Phù hợp với số lượng props vừa phải (5-7 props)
  • Sẽ chọn Approach B nếu:
    • Data từ API (đã có sẵn object)
    • Quá nhiều props (>10)
    • Props thường đi cùng nhau

⭐⭐⭐ Exercise 3: Product Grid (40 phút)

🎯 Mục tiêu: Xây dựng product listing page
⏱️ Thời gian: 40 phút

📋 Product Requirements: User Story: "Là khách hàng, tôi muốn xem danh sách sản phẩm với hình ảnh, tên, giá, và rating để có thể chọn mua."

✅ Acceptance Criteria:

  • [ ] Hiển thị grid layout (3 columns)
  • [ ] Mỗi product card có: image, name, price, rating (stars)
  • [ ] Hover vào card có effect (scale hoặc shadow)
  • [ ] Giá format đúng ($1,234.56)
  • [ ] Rating hiển thị sao vàng (★) và sao xám (☆)

🎨 Technical Constraints:

  • Sử dụng component ProductCard
  • Không hard-code products trong ProductCard
  • Dữ liệu products lưu trong array

🚨 Edge Cases cần handle:

  • Product không có image → hiển thị placeholder
  • Rating = 0 → hiển thị "No ratings yet"
  • Price = 0 → hiển thị "Free"
  • Name quá dài → truncate với "..."
jsx
// Sample data
const products = [
  {
    id: 1,
    name: 'Wireless Headphones',
    price: 99.99,
    image: 'headphones.jpg',
    rating: 4.5,
  },
  {
    id: 2,
    name: 'Smart Watch with Amazing Features and Long Battery Life',
    price: 299.99,
    image: '',
    rating: 5,
  },
  {
    id: 3,
    name: 'Bluetooth Speaker',
    price: 0,
    image: 'speaker.jpg',
    rating: 0,
  },
];

// 🎯 NHIỆM VỤ:

// 1. Tạo ProductCard component
function ProductCard(/* TODO */) {
  // TODO: Handle edge cases
  return <div className='product-card'>{/* TODO: Implement */}</div>;
}

// 2. Tạo ProductGrid component
function ProductGrid(/* TODO */) {
  return (
    <div className='product-grid'>
      {/* TODO: Map products to ProductCard */}
    </div>
  );
}

// 3. Render trong App
function App() {
  return (
    <div className='container'>
      <h1>Our Products</h1>
      <ProductGrid products={products} />
    </div>
  );
}

📝 Implementation Checklist:

  • [ ] ProductCard component với tất cả fields
  • [ ] Handle missing image
  • [ ] Format price correctly
  • [ ] Render rating stars
  • [ ] Truncate long names
  • [ ] Grid layout responsive
  • [ ] Hover effects
💡 Solution
markdown

### 📘 Kiến thức cũ: React render được gì?

### ✅ React render được

React có thể render các kiểu dữ liệu sau:

- `string`
- `number`
- `boolean` *(bị bỏ qua, không hiển thị)*
- `null`, `undefined` *(bị bỏ qua)*
- **React element** (ví dụ: `<span />`)
- **Array các React element**

```jsx
{"Hello"}          // ✅ OK
{123}              // ✅ OK
{false}            // ❌ Không hiển thị
{null}             // ❌ Không hiển thị
{undefined}        // ❌ Không hiển thị
{<span>Hi</span>}  // ✅ OK
{[<span />, <span />]} // ✅ OK
```

---

### ❌ React KHÔNG render được

* **Object thuần (`{}`)**

```jsx
{{ name: "John" }} // ❌ Error
```

👉 Muốn hiển thị object → cần **map**, **convert sang string**, hoặc **render từng field**

Áp dụng vào để viết các helper:

jsx

// Helper function: Format price
function formatPrice(price) {
  if (typeof price !== 'number' || Number.isNaN(price)) {
    return '';
  }
  if (price === 0) return 'Free';
  return `$${price.toLocaleString('en-US', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  })}`;
}

//🧠 Nếu muốn “chuẩn chỉnh” hơn nữa dùng (Intl)
const formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  minimumFractionDigits: 2,
});

function formatPrice(price) {
  if (typeof price !== 'number' || Number.isNaN(price)) return '';
  if (price === 0) return 'Free';
  return formatter.format(price);
}


// Cần validate input vì có thể xảy ra các lỗi như:

// formatPrice("100");   // ❌ crash
// formatPrice(null);    // ❌ crash
// formatPrice(undefined); // ❌ crash
// formatPrice(NaN);   // "$NaN"
// formatPrice(-10);  // số âm "$-10.00" (có thể không mong muốn)

//---------------------------------------------

// Helper function: Render rating stars
function renderStars(rating) {
  if (rating === 0) return <span className='no-rating'>No ratings yet</span>;

  const stars = [];
  const fullStars = Math.floor(rating);
  const hasHalfStar = rating % 1 !== 0;

  // Full stars
  for (let i = 0; i < fullStars; i++) {
    stars.push(
      <span
        key={`full-${i}`}
        className='star full'
      >

      </span>
    );
  }

  // Half star
  if (hasHalfStar) {
    stars.push(
      <span
        key='half'
        className='star half'
      >

      </span>
    );
  }

  // Empty stars
  const emptyStars = 5 - Math.ceil(rating);
  for (let i = 0; i < emptyStars; i++) {
    stars.push(
      <span
        key={`empty-${i}`}
        className='star empty'
      >

      </span>
    );
  }

  return <div className='rating'>{stars}</div>;
}

//Hoặc đơn giản hoá logic
const renderRatingShort = (rating) => {
  const stars = [];

  for (let i = 1; i <= 5; i++) {
    stars.push(<span key={i}>{i <= rating ? '⭐' : '☆'}</span>);
  }

  return stars;
};

// Hoặc gọn hơn với map
const renderRatingWithMap = (rating) =>
  Array.from({ length: 5 }, (_, i) => (
    <span key={i}>{i + 1 <= rating ? '⭐' : '☆'}</span>
  ));

// ProductCard Component
function ProductCard({ id, name, price, image, rating }) {
  const placeholderImage = 'https://via.placeholder.com/300x200?text=No+Image';
  const displayImage = image || placeholderImage;

  // Truncate name if too long
  const displayName = name.length > 50 ? name.substring(0, 47) + '...' : name;

  return (
    <div className='product-card'>
      <div className='product-image-wrapper'>
        <img
          src={displayImage}
          alt={name}
          className='product-image'
        />
      </div>
      <div className='product-info'>
        <h3
          className='product-name'
          title={name}
        >
          {displayName}
        </h3>
        <div className='product-rating'>{renderStars(rating)}</div>
        <div className='product-price'>{formatPrice(price)}</div>
      </div>
    </div>
  );
}

// ProductGrid Component
function ProductGrid({ products }) {
  return (
    <div className='product-grid'>
      {products.map((product) => (
        <ProductCard
          key={product.id}
          id={product.id}
          name={product.name}
          price={product.price}
          image={product.image}
          rating={product.rating}
        />
      ))}
    </div>
  );
}

// App Component
function App() {
  const products = [
    {
      id: 1,
      name: 'Wireless Headphones',
      price: 99.99,
      image:
        'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300',
      rating: 4.5,
    },
    {
      id: 2,
      name: 'Smart Watch with Amazing Features and Long Battery Life That Everyone Loves',
      price: 299.99,
      image: '',
      rating: 5,
    },
    {
      id: 3,
      name: 'Bluetooth Speaker',
      price: 0,
      image:
        'https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=300',
      rating: 0,
    },
    {
      id: 4,
      name: 'USB-C Cable',
      price: 12.99,
      image:
        'https://images.unsplash.com/photo-1589003077984-894e133dabab?w=300',
      rating: 4,
    },
  ];

  return (
    <div className='container'>
      <h1>Our Products</h1>
      <ProductGrid products={products} />
    </div>
  );
}

CSS:

css
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
  margin-top: 24px;
}

.product-card {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
  transition: transform 0.2s, box-shadow 0.2s;
  cursor: pointer;
}

.product-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.product-image-wrapper {
  width: 100%;
  height: 200px;
  overflow: hidden;
  background: #f5f5f5;
}

.product-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.product-info {
  padding: 16px;
}

.product-name {
  font-size: 16px;
  font-weight: 600;
  margin: 0 0 8px 0;
  color: #333;
  min-height: 40px;
}

.product-rating {
  margin: 8px 0;
}

.star {
  font-size: 18px;
  margin-right: 2px;
}

.star.full {
  color: #ffc107;
}

.star.half {
  color: #ffc107;
  opacity: 0.6;
}

.star.empty {
  color: #e0e0e0;
}

.no-rating {
  font-size: 12px;
  color: #999;
  font-style: italic;
}

.product-price {
  font-size: 20px;
  font-weight: bold;
  color: #1976d2;
  margin-top: 8px;
}

📚 Giải thích kỹ thuật:

  1. Helper Functions:

    • formatPrice(): Xử lý format tiền tệ, handle edge case price = 0
    • renderStars(): Logic render sao dựa trên rating, handle rating = 0
  2. Component Separation:

    • ProductCard: Presentational component, chỉ hiển thị 1 product
    • ProductGrid: Container component, map array products
  3. Edge Case Handling:

    • Missing image → placeholder
    • Long name → truncate + show full name on hover (title attribute)
    • Price = 0 → "Free"
    • Rating = 0 → "No ratings yet"
  4. Props Spread:

    • Có thể dùng <ProductCard {...product} /> thay vì truyền từng prop
    • Trade-off: Ngắn gọn hơn nhưng ít explicit hơn
  5. Key Prop:

    • key={product.id} trong .map() để React track elements
    • Sẽ học kỹ hơn ở Ngày 6 (Lists & Keys)

⭐⭐⭐⭐ Exercise 4: Layout System (60 phút)

🎯 Mục tiêu: Tạo reusable layout components
⏱️ Thời gian: 60 phút

🏗️ PHASE 1: Research & Design (20 phút)

Nhiệm vụ: Thiết kế một layout system linh hoạt cho web app. Cần có:

  1. Container (giới hạn max-width, center content)
  2. Row/Column layout (flexbox-based)
  3. Card wrapper
  4. Header/Footer

So sánh các approaches:

Approach A: Single flexible Container

jsx
<Container
  maxWidth='lg'
  padding='large'
>
  {/* Content */}
</Container>
  • Pros: Đơn giản, dễ dùng
  • Cons: Ít control, khó customize sâu

Approach B: Composed Components

jsx
<Container>
  <Row>
    <Column span={6}>Left</Column>
    <Column span={6}>Right</Column>
  </Row>
</Container>
  • Pros: Linh hoạt, powerful
  • Cons: Nhiều components, phức tạp hơn

Approach C: CSS Utility Classes (Tailwind-style)

jsx
<div className='container mx-auto px-4'>
  <div className='flex gap-4'>
    <div className='flex-1'>Left</div>
    <div className='flex-1'>Right</div>
  </div>
</div>
  • Pros: Nhanh, không cần JS
  • Cons: Mixing concerns, nhiều class names

💭 CHỌN APPROACH VÀ VIẾT ADR:

markdown
## ADR: Layout System Design

### Context

Cần xây dựng layout system tái sử dụng cho toàn bộ app.
Requirements: Responsive, flexible, maintainable.

### Decision

[Approach bạn chọn]

### Rationale

[Tại sao chọn approach này]

- Lý do 1:
- Lý do 2:
- Lý do 3:

### Consequences

**Positive:**

- [Ưu điểm 1]
- [Ưu điểm 2]

**Negative:**

- [Nhược điểm 1]
- [Nhược điểm 2]

### Alternatives Considered

- [Approach A]: Pros/Cons
- [Approach C]: Pros/Cons

💻 PHASE 2: Implementation (30 phút)

Implement approach bạn đã chọn. Ví dụ với Approach B:

jsx
// 🎯 NHIỆM VỤ: Implement các components sau

// 1. Container Component
function Container({ children, maxWidth = '1200px' }) {
  // TODO
}

// 2. Row Component (flexbox wrapper)
function Row({ children, gap = '16px', justify = 'flex-start' }) {
  // TODO
}

// 3. Column Component
function Column({ children, span = 12 }) {
  // span: 1-12 (12-column grid system)
  // TODO
}

// 4. Card Component
function Card({ children, title, padding = '16px' }) {
  // TODO
}

// Test Layout
function App() {
  return (
    <Container>
      <Row gap='24px'>
        <Column span={8}>
          <Card title='Main Content'>
            <p>Lorem ipsum...</p>
          </Card>
        </Column>
        <Column span={4}>
          <Card title='Sidebar'>
            <ul>
              <li>Link 1</li>
              <li>Link 2</li>
            </ul>
          </Card>
        </Column>
      </Row>
    </Container>
  );
}

🧪 PHASE 3: Testing (10 phút)

Manual Testing Checklist:

  • [ ] Container centers content
  • [ ] Row spacing works correctly
  • [ ] Column spans add up to 12
  • [ ] Card displays title and content
  • [ ] Responsive on mobile (stack columns)
💡 Solution
jsx
// Container Component
function Container({ children, maxWidth = '1200px' }) {
  return (
    <div
      style={{
        maxWidth,
        margin: '0 auto',
        padding: '0 20px',
      }}
    >
      {children}
    </div>
  );
}

// Row Component
function Row({ children, gap = '16px', justify = 'flex-start' }) {
  return (
    <div
      style={{
        display: 'flex',
        gap,
        justifyContent: justify,
        flexWrap: 'wrap',
      }}
    >
      {children}
    </div>
  );
}

// Column Component
function Column({ children, span = 12 }) {
  // 12-column grid: span=6 → 50%, span=4 → 33.33%
  const widthPercent = (span / 12) * 100;

  return (
    <div
      style={{
        flex: `0 0 ${widthPercent}%`,
        maxWidth: `${widthPercent}%`,
        minWidth: '280px', // Responsive: stack on mobile
      }}
    >
      {children}
    </div>
  );
}

// Card Component
function Card({ children, title, padding = '16px' }) {
  return (
    <div
      style={{
        border: '1px solid #e0e0e0',
        borderRadius: '8px',
        padding,
        backgroundColor: 'white',
        boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
      }}
    >
      {title && (
        <h3
          style={{
            margin: '0 0 16px 0',
            fontSize: '18px',
            fontWeight: '600',
            borderBottom: '2px solid #f0f0f0',
            paddingBottom: '8px',
          }}
        >
          {title}
        </h3>
      )}
      {children}
    </div>
  );
}

// Demo App
function App() {
  return (
    <Container maxWidth='1400px'>
      <h1 style={{ textAlign: 'center', margin: '40px 0' }}>
        Dashboard Layout
      </h1>

      {/* Hero Section */}
      <Card
        title='Welcome'
        padding='32px'
      >
        <p style={{ fontSize: '18px', lineHeight: 1.6 }}>
          This is a demo of a flexible layout system built with React
          components.
        </p>
      </Card>

      {/* Two Column Layout */}
      <Row
        gap='24px'
        style={{ marginTop: '24px' }}
      >
        <Column span={8}>
          <Card title='Main Content'>
            <p>
              Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
              eiusmod tempor incididunt ut labore et dolore magna aliqua.
            </p>
            <p>
              Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
              nisi ut aliquip ex ea commodo consequat.
            </p>
          </Card>
        </Column>

        <Column span={4}>
          <Card title='Quick Links'>
            <ul style={{ listStyle: 'none', padding: 0 }}>
              <li style={{ marginBottom: '8px' }}>
                <a
                  href='#'
                  style={{ color: '#1976d2' }}
                >
                  Dashboard
                </a>
              </li>
              <li style={{ marginBottom: '8px' }}>
                <a
                  href='#'
                  style={{ color: '#1976d2' }}
                >
                  Profile
                </a>
              </li>
              <li style={{ marginBottom: '8px' }}>
                <a
                  href='#'
                  style={{ color: '#1976d2' }}
                >
                  Settings
                </a>
              </li>
            </ul>
          </Card>
        </Column>
      </Row>

      {/* Three Column Layout */}
      <Row
        gap='16px'
        style={{ marginTop: '24px' }}
      >
        <Column span={4}>
          <Card title='Stats'>
            <p
              style={{ fontSize: '32px', fontWeight: 'bold', color: '#1976d2' }}
            >
              1,234
            </p>
            <p style={{ color: '#666' }}>Total Users</p>
          </Card>
        </Column>

        <Column span={4}>
          <Card title='Revenue'>
            <p
              style={{ fontSize: '32px', fontWeight: 'bold', color: '#2e7d32' }}
            >
              $45,678
            </p>
            <p style={{ color: '#666' }}>This Month</p>
          </Card>
        </Column>

        <Column span={4}>
          <Card title='Orders'>
            <p
              style={{ fontSize: '32px', fontWeight: 'bold', color: '#ed6c02' }}
            >
              567
            </p>
            <p style={{ color: '#666' }}>Pending</p>
          </Card>
        </Column>
      </Row>
    </Container>
  );
}

💡 Giải thích thiết kế:

  1. Container:

    • Giới hạn max-width để content không quá rộng
    • Center với margin: 0 auto
    • Padding để không chạm sát edge
  2. Row:

    • Flexbox với flexWrap: "wrap" → responsive
    • Gap thay vì margin cho spacing đồng nhất
    • Justify prop để align ngang
  3. Column:

    • 12-column grid system (như Bootstrap)
    • flex: 0 0 X% để control width chính xác
    • minWidth: 280px → stack trên mobile
  4. Card:

    • Border, shadow cho depth
    • Optional title với border-bottom
    • Flexible padding

🎯 Best Practices:

  • ✅ Inline styles cho demo (production nên dùng CSS)
  • ✅ Default values cho props
  • ✅ Flexible, composable components
  • ✅ Responsive by default (flexWrap, minWidth)

⭐⭐⭐⭐⭐ Exercise 5: Blog Post Viewer (90 phút)

🎯 Mục tiêu: Build production-ready blog viewer
⏱️ Thời gian: 90 phút

📋 Feature Specification:

User Story: "Là độc giả, tôi muốn đọc bài blog với formatting đẹp, có table of contents, và có thể share bài viết."

✅ Production Checklist:

UI Components:

  • [ ] BlogPost component (title, meta, content, author)
  • [ ] AuthorCard component (avatar, name, bio)
  • [ ] TableOfContents component (auto-generate từ headings)
  • [ ] ShareButtons component (Facebook, Twitter, LinkedIn)
  • [ ] RelatedPosts component (3 bài liên quan)

Features:

  • [ ] Format date (e.g., "January 20, 2025")
  • [ ] Reading time estimate (words / 200 wpm)
  • [ ] Responsive layout (mobile-first)
  • [ ] Accessibility (semantic HTML, ARIA)
  • [ ] SEO meta tags (title, description)

Edge Cases:

  • [ ] Long titles → truncate/wrap gracefully
  • [ ] Missing author avatar → default avatar
  • [ ] No related posts → hide section
  • [ ] Very short content → adjust layout
jsx
// Sample Data
const blogPost = {
  id: 1,
  title: 'Getting Started with React Components and Props',
  slug: 'react-components-props',
  content: `
# Introduction

React components are the building blocks of React applications...

## What are Components?

Components let you split the UI into independent, reusable pieces...

## Props Explained

Props (short for properties) are how you pass data from parent to child...

## Best Practices

1. Keep components small and focused
2. Use prop destructuring
3. Provide default values
  `,
  author: {
    id: 1,
    name: 'Sarah Johnson',
    avatar: 'https://i.pravatar.cc/150?img=5',
    bio: 'Senior Frontend Developer with 8 years of experience in React.',
  },
  publishedAt: '2025-01-20T10:30:00Z',
  tags: ['React', 'JavaScript', 'Frontend'],
  category: 'Tutorial',
};

const relatedPosts = [
  {
    id: 2,
    title: 'Understanding React State',
    slug: 'react-state',
    excerpt: 'Learn how to manage state in React components...',
    thumbnail: 'https://picsum.photos/seed/2/400/250',
  },
  {
    id: 3,
    title: 'React Hooks Deep Dive',
    slug: 'react-hooks',
    excerpt: 'Master React hooks with practical examples...',
    thumbnail: 'https://picsum.photos/seed/3/400/250',
  },
];

// 🎯 NHIỆM VỤ CỦA BẠN:

// 1. BlogPost Component
function BlogPost({ post }) {
  // TODO: Implement
}

// 2. AuthorCard Component
function AuthorCard({ author }) {
  // TODO: Implement
}

// 3. TableOfContents Component
function TableOfContents({ content }) {
  // Extract headings from markdown content
  // TODO: Implement
}

// 4. ShareButtons Component
function ShareButtons({ title, url }) {
  // TODO: Implement
}

// 5. RelatedPosts Component
function RelatedPosts({ posts }) {
  // TODO: Implement
}

// Main App
function App() {
  return (
    <div className='blog-container'>
      <BlogPost post={blogPost} />
      <RelatedPosts posts={relatedPosts} />
    </div>
  );
}

📝 Implementation Hints:

  1. Date Formatting:
javascript
const formatDate = (dateString) => {
  const date = new Date(dateString);
  return date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  });
};
  1. Reading Time:
javascript
const calculateReadingTime = (text) => {
  const wordsPerMinute = 200;
  const words = text.trim().split(/\s+/).length;
  return Math.ceil(words / wordsPerMinute);
};
  1. Extract Headings:
javascript
const extractHeadings = (markdown) => {
  const headingRegex = /^(#{1,3})\s+(.+)$/gm;
  const headings = [];
  let match;

  while ((match = headingRegex.exec(markdown)) !== null) {
    headings.push({
      level: match[1].length,
      text: match[2],
    });
  }

  return headings;
};
💡 Solution
jsx
// ======================
// HELPER FUNCTIONS
// ======================

const formatDate = (dateString) => {
  const date = new Date(dateString);
  return date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  });
};

const calculateReadingTime = (text) => {
  const wordsPerMinute = 200;
  const words = text.trim().split(/\s+/).length;
  const minutes = Math.ceil(words / wordsPerMinute);
  return `${minutes} min read`;
};

const extractHeadings = (markdown) => {
  const headingRegex = /^(#{1,3})\s+(.+)$/gm;
  const headings = [];
  let match;

  while ((match = headingRegex.exec(markdown)) !== null) {
    headings.push({
      level: match[1].length,
      text: match[2],
      id: match[2].toLowerCase().replace(/\s+/g, '-'),
    });
  }

  return headings;
};

// ======================
// COMPONENTS
// ======================

// AuthorCard Component
function AuthorCard({ author }) {
  const defaultAvatar =
    'https://ui-avatars.com/api/?name=' + encodeURIComponent(author.name);

  return (
    <div className='author-card'>
      <img
        src={author.avatar || defaultAvatar}
        alt={author.name}
        className='author-avatar'
      />
      <div className='author-info'>
        <h4 className='author-name'>{author.name}</h4>
        <p className='author-bio'>{author.bio}</p>
      </div>
    </div>
  );
}

// TableOfContents Component
function TableOfContents({ content }) {
  const headings = extractHeadings(content);

  if (headings.length === 0) return null;

  return (
    <nav
      className='table-of-contents'
      aria-label='Table of contents'
    >
      <h3 className='toc-title'>Table of Contents</h3>
      <ul className='toc-list'>
        {headings.map((heading, index) => (
          <li
            key={index}
            className={`toc-item toc-level-${heading.level}`}
          >
            <a
              href={`#${heading.id}`}
              className='toc-link'
            >
              {heading.text}
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
}

// ShareButtons Component
function ShareButtons({ title, url }) {
  const shareUrl = encodeURIComponent(url);
  const shareTitle = encodeURIComponent(title);

  const shareLinks = {
    facebook: `https://www.facebook.com/sharer/sharer.php?u=${shareUrl}`,
    twitter: `https://twitter.com/intent/tweet?text=${shareTitle}&url=${shareUrl}`,
    linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}`,
  };

  const handleShare = (platform) => {
    console.log(`Sharing to ${platform}: ${title}`);
    // In production: window.open(shareLinks[platform], '_blank')
  };

  return (
    <div className='share-buttons'>
      <h4 className='share-title'>Share this article</h4>
      <div className='share-buttons-group'>
        <button
          className='share-btn share-btn-facebook'
          onClick={() => handleShare('facebook')}
          aria-label='Share on Facebook'
        >
          📘 Facebook
        </button>
        <button
          className='share-btn share-btn-twitter'
          onClick={() => handleShare('twitter')}
          aria-label='Share on Twitter'
        >
          🐦 Twitter
        </button>
        <button
          className='share-btn share-btn-linkedin'
          onClick={() => handleShare('linkedin')}
          aria-label='Share on LinkedIn'
        >
          💼 LinkedIn
        </button>
      </div>
    </div>
  );
}

// RelatedPosts Component
function RelatedPosts({ posts }) {
  if (!posts || posts.length === 0) return null;

  return (
    <aside
      className='related-posts'
      aria-label='Related articles'
    >
      <h3 className='related-title'>Related Articles</h3>
      <div className='related-grid'>
        {posts.map((post) => (
          <article
            key={post.id}
            className='related-card'
          >
            <img
              src={post.thumbnail}
              alt={post.title}
              className='related-thumbnail'
            />
            <div className='related-content'>
              <h4 className='related-card-title'>{post.title}</h4>
              <p className='related-excerpt'>{post.excerpt}</p>
              <a
                href={`/blog/${post.slug}`}
                className='related-link'
              >
                Read more →
              </a>
            </div>
          </article>
        ))}
      </div>
    </aside>
  );
}

// BlogPost Component
function BlogPost({ post }) {
  const readingTime = calculateReadingTime(post.content);
  const formattedDate = formatDate(post.publishedAt);

  return (
    <article className='blog-post'>
      {/* Header */}
      <header className='post-header'>
        <h1 className='post-title'>{post.title}</h1>

        <div className='post-meta'>
          <time
            dateTime={post.publishedAt}
            className='post-date'
          >
            {formattedDate}
          </time>
          <span className='meta-separator'>•</span>
          <span className='post-reading-time'>{readingTime}</span>
          <span className='meta-separator'>•</span>
          <span className='post-category'>{post.category}</span>
        </div>

        <div className='post-tags'>
          {post.tags.map((tag) => (
            <span
              key={tag}
              className='tag'
            >
              #{tag}
            </span>
          ))}
        </div>
      </header>

      {/* Author */}
      <AuthorCard author={post.author} />

      {/* Table of Contents */}
      <TableOfContents content={post.content} />

      {/* Content */}
      <div className='post-content'>
        {/* In production, use markdown parser */}
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </div>

      {/* Footer */}
      <footer className='post-footer'>
        <ShareButtons
          title={post.title}
          url={`https://example.com/blog/${post.slug}`}
        />
      </footer>
    </article>
  );
}

// Main App
function App() {
  const blogPost = {
    id: 1,
    title: 'Getting Started with React Components and Props',
    slug: 'react-components-props',
    content: `
# Introduction

React components are the building blocks of React applications...

## What are Components?

Components let you split the UI into independent, reusable pieces...

## Props Explained

Props (short for properties) are how you pass data from parent to child...

## Best Practices

1. Keep components small and focused
2. Use prop destructuring
3. Provide default values
  `,
    author: {
      id: 1,
      name: 'Sarah Johnson',
      avatar: 'https://i.pravatar.cc/150?img=5',
      bio: 'Senior Frontend Developer with 8 years of experience in React.',
    },
    publishedAt: '2025-01-20T10:30:00Z',
    tags: ['React', 'JavaScript', 'Frontend'],
    category: 'Tutorial',
  };

  const relatedPosts = [
    {
      id: 2,
      title: 'Understanding React State Management',
      slug: 'react-state',
      excerpt:
        'Learn how to manage state in React components effectively with useState and useReducer hooks.',
      thumbnail: 'https://picsum.photos/seed/2/400/250',
    },
    {
      id: 3,
      title: 'React Hooks Deep Dive',
      slug: 'react-hooks',
      excerpt:
        'Master React hooks with practical examples and best practices for modern React development.',
      thumbnail: 'https://picsum.photos/seed/3/400/250',
    },
    {
      id: 4,
      title: 'Building Reusable Components',
      slug: 'reusable-components',
      excerpt:
        'Discover patterns and techniques for creating truly reusable React components.',
      thumbnail: 'https://picsum.photos/seed/4/400/250',
    },
  ];

  return (
    <div className='blog-container'>
      <BlogPost post={blogPost} />
      <RelatedPosts posts={relatedPosts} />
    </div>
  );
}

CSS (Đầy đủ):

css
/* Container */
.blog-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 40px 20px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  line-height: 1.6;
  color: #333;
}

/* Blog Post */
.blog-post {
  background: white;
}

/* Header */
.post-header {
  margin-bottom: 32px;
  padding-bottom: 24px;
  border-bottom: 2px solid #f0f0f0;
}

.post-title {
  font-size: 36px;
  font-weight: 700;
  margin: 0 0 16px 0;
  line-height: 1.2;
  color: #1a1a1a;
}

.post-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: #666;
  margin-bottom: 12px;
}

.meta-separator {
  color: #ccc;
}

.post-category {
  color: #1976d2;
  font-weight: 500;
}

.post-tags {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.tag {
  padding: 4px 12px;
  background: #f0f0f0;
  border-radius: 16px;
  font-size: 12px;
  color: #666;
}

/* Author Card */
.author-card {
  display: flex;
  gap: 16px;
  padding: 16px;
  background: #f9f9f9;
  border-radius: 8px;
  margin: 24px 0;
}

.author-avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  object-fit: cover;
}

.author-info {
  flex: 1;
}

.author-name {
  margin: 0 0 4px 0;
  font-size: 16px;
  font-weight: 600;
  color: #1a1a1a;
}

.author-bio {
  margin: 0;
  font-size: 14px;
  color: #666;
  line-height: 1.4;
}

/* Table of Contents */
.table-of-contents {
  background: #f9f9f9;
  border-left: 4px solid #1976d2;
  padding: 16px 20px;
  margin: 24px 0;
  border-radius: 4px;
}

.toc-title {
  margin: 0 0 12px 0;
  font-size: 16px;
  font-weight: 600;
  color: #1a1a1a;
}

.toc-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.toc-item {
  margin: 8px 0;
}

.toc-level-2 {
  padding-left: 0;
}

.toc-level-3 {
  padding-left: 20px;
}

.toc-link {
  color: #1976d2;
  text-decoration: none;
  font-size: 14px;
  transition: color 0.2s;
}

.toc-link:hover {
  color: #1565c0;
  text-decoration: underline;
}

/* Content */
.post-content {
  margin: 32px 0;
  font-size: 16px;
  line-height: 1.8;
}

.post-content h2 {
  font-size: 28px;
  margin: 32px 0 16px 0;
  color: #1a1a1a;
}

.post-content h3 {
  font-size: 22px;
  margin: 24px 0 12px 0;
  color: #333;
}

.post-content p {
  margin: 16px 0;
}

.post-content ol,
.post-content ul {
  margin: 16px 0;
  padding-left: 24px;
}

.post-content li {
  margin: 8px 0;
}

/* Share Buttons */
.post-footer {
  margin-top: 48px;
  padding-top: 24px;
  border-top: 2px solid #f0f0f0;
}

.share-buttons {
  text-align: center;
}

.share-title {
  font-size: 16px;
  font-weight: 600;
  margin: 0 0 16px 0;
  color: #666;
}

.share-buttons-group {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
}

.share-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: transform 0.2s, opacity 0.2s;
}

.share-btn:hover {
  transform: translateY(-2px);
  opacity: 0.9;
}

.share-btn-facebook {
  background: #1877f2;
  color: white;
}

.share-btn-twitter {
  background: #1da1f2;
  color: white;
}

.share-btn-linkedin {
  background: #0a66c2;
  color: white;
}

/* Related Posts */
.related-posts {
  margin-top: 64px;
}

.related-title {
  font-size: 24px;
  font-weight: 700;
  margin: 0 0 24px 0;
  color: #1a1a1a;
}

.related-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 24px;
}

.related-card {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  overflow: hidden;
  transition: transform 0.2s, box-shadow 0.2s;
}

.related-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.related-thumbnail {
  width: 100%;
  height: 150px;
  object-fit: cover;
}

.related-content {
  padding: 16px;
}

.related-card-title {
  font-size: 16px;
  font-weight: 600;
  margin: 0 0 8px 0;
  color: #1a1a1a;
  line-height: 1.3;
}

.related-excerpt {
  font-size: 14px;
  color: #666;
  margin: 0 0 12px 0;
  line-height: 1.4;
}

.related-link {
  color: #1976d2;
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
  display: inline-block;
  transition: color 0.2s;
}

.related-link:hover {
  color: #1565c0;
  text-decoration: underline;
}

/* Responsive */
@media (max-width: 768px) {
  .blog-container {
    padding: 20px 16px;
  }

  .post-title {
    font-size: 28px;
  }

  .post-meta {
    flex-wrap: wrap;
  }

  .author-card {
    flex-direction: column;
    text-align: center;
  }

  .author-avatar {
    margin: 0 auto;
  }

  .share-buttons-group {
    flex-direction: column;
  }

  .share-btn {
    width: 100%;
  }

  .related-grid {
    grid-template-columns: 1fr;
  }
}

📚 Giải thích Production Best Practices:

  1. Accessibility:

    • Semantic HTML (<article>, <header>, <footer>, <nav>)
    • ARIA labels cho screen readers
    • dateTime attribute cho <time> element
  2. SEO:

    • Proper heading hierarchy (h1 → h2 → h3)
    • Meta tags (publishedAt, author, category)
    • Structured content
  3. Performance:

    • Conditional rendering (hide empty sections)
    • Efficient re-renders (pure components)
    • Image lazy loading (production: add loading="lazy")
  4. UX:

    • Reading time estimate
    • Table of contents for navigation
    • Related posts for engagement
    • Share buttons for virality
  5. Maintainability:

    • Helper functions separated
    • Components single-responsibility
    • Consistent naming conventions
    • CSS bem-like naming

📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)

Bảng So Sánh Trade-offs

PatternPros ✅Cons ❌When to Use
Props Object
function Card(props)
- Dễ thêm props mới
- Access props object nếu cần
- Dùng spread: {...props}
- Verbose (props.title)
- Không biết props có gì
- Component nhận nhiều props (>10)
- Props thay đổi thường xuyên
Props Destructuring
function Card({ title, content })
- Clean, concise
- IDE autocomplete
- Rõ ràng props nào được dùng
- Phải list tất cả props
- Khó spread remaining props
- RECOMMENDED cho mọi case
- Props ít/vừa (<10)
Default Values
function Card({ title = "Untitled" })
- Handle missing props
- Tránh undefined errors
- Self-documenting
- Có thể ẩn bugs
- Cần cẩn thận với falsy values
- Optional props
- Fallback values
- Configuration props
Children Prop
<Card>{content}</Card>
- Maximum flexibility
- Composition-friendly
- Any content type
- Ít type-safe
- Khó validate content
- Wrapper components
- Layouts
- Dynamic content
Render Props
<Card render={() => ...} />
- Extreme flexibility
- Logic reuse
- Complex, verbose
- Performance cost
- ⚠️ Legacy pattern
- Dùng hooks thay thế

Decision Tree

Cần truyền data từ Parent → Child?

├─ YES → Dùng Props
│   │
│   ├─ Content phức tạp/dynamic?
│   │   ├─ YES → Dùng Children Prop
│   │   └─ NO → Dùng regular props
│   │
│   ├─ Bao nhiêu props?
│   │   ├─ 1-7 props → Destructuring
│   │   ├─ 8-15 props → Destructuring + grouping
│   │   └─ >15 props → Object prop hoặc refactor component
│   │
│   └─ Props có thể undefined?
│       ├─ YES → Default values
│       └─ NO → Regular destructuring

└─ NO → Component tự quản lý data
    └─ Sẽ học State ở Ngày 11

🧪 PHẦN 5: DEBUG LAB (20 phút)

Bug 1: Props Mutation ❌

jsx
// 🐛 CODE BỊ LỖI:
function Counter(props) {
  props.count = props.count + 1; // ❌ Sửa props!
  return <div>Count: {props.count}</div>;
}

function App() {
  return <Counter count={5} />;
}

❓ Câu hỏi:

  1. Lỗi gì xảy ra?
  2. Tại sao không được sửa props?
  3. Làm thế nào để "increment" count?
💡 Solution

1. Lỗi:

  • React warning: "Cannot assign to read only property"
  • Behavior không đoán trước được
  • Component không re-render

2. Tại sao sai:

  • Props là immutable (read-only)
  • Sửa props phá vỡ one-way data flow
  • Parent không biết child sửa data → inconsistency

3. Giải pháp:

jsx
// ❌ SAI: Sửa props
props.count = props.count + 1;

// ✅ ĐÚNG: Sẽ học State ở Ngày 11
// (Hiện tại chưa học state nên chưa thể làm)

// Tạm thời: Tính toán derived value
function Counter({ count }) {
  const displayCount = count + 1;
  return <div>Count: {displayCount}</div>;
}

🎯 Nguyên tắc vàng: Props là read-only. Nếu cần thay đổi data, dùng State (sẽ học sau).


Bug 2: Component Name Lowercase ❌

jsx
// 🐛 CODE BỊ LỖI:
function userProfile({ name, email }) {
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <userProfile
        name='John'
        email='john@example.com'
      />
    </div>
  );
}

❓ Câu hỏi:

  1. Component có render không?
  2. Lỗi gì trong console?
  3. Tại sao phải viết hoa?
💡 Solution

1. Không render:

  • React treat <userProfile> như HTML tag
  • Browser không hiểu tag <userprofile> → ignore

2. Lỗi console:

Warning: The tag <userProfile> is unrecognized in this browser.

3. Tại sao phải viết hoa:

jsx
// ❌ Lowercase → HTML tag
<div> <span> <userprofile>

// ✅ PascalCase → React Component
<Div> <Span> <UserProfile>

React phân biệt:

  • Lowercase = built-in HTML tag
  • PascalCase = custom component

✅ FIX:

jsx
// Đổi tên function thành PascalCase
function UserProfile({ name, email }) {
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <UserProfile
        name='John'
        email='john@example.com'
      />
    </div>
  );
}

Bug 3: Undefined Props ❌

jsx
// 🐛 CODE BỊ LỖI:
function ProductCard({ name, price, image }) {
  return (
    <div className='card'>
      <img
        src={image}
        alt={name}
      />
      <h3>{name.toUpperCase()}</h3>
      <p>${price.toFixed(2)}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <ProductCard
        name='iPhone'
        price={999}
        image='phone.jpg'
      />
      <ProductCard
        price={799}
        image='laptop.jpg'
      />
      {/* Forgot to pass 'name' prop! */}
    </div>
  );
}

❓ Câu hỏi:

  1. Lỗi gì xảy ra?
  2. Dòng code nào gây lỗi?
  3. Làm sao prevent lỗi này?
💡 Solution

1. Lỗi:

TypeError: Cannot read property 'toUpperCase' of undefined

2. Dòng lỗi:

jsx
<h3>{name.toUpperCase()}</h3>
// name = undefined (không truyền prop)
// undefined.toUpperCase() → ERROR!

3. Solutions:

Solution 1: Default values

jsx
function ProductCard({
  name = 'Unnamed Product', // Default value
  price,
  image,
}) {
  return (
    <div className='card'>
      <img
        src={image}
        alt={name}
      />
      <h3>{name.toUpperCase()}</h3>
      <p>${price.toFixed(2)}</p>
    </div>
  );
}

Solution 2: Conditional rendering

jsx
function ProductCard({ name, price, image }) {
  if (!name || !price) {
    return <div className='card error'>Invalid product data</div>;
  }

  return (
    <div className='card'>
      <img
        src={image}
        alt={name}
      />
      <h3>{name.toUpperCase()}</h3>
      <p>${price.toFixed(2)}</p>
    </div>
  );
}

Solution 3: Optional chaining (RECOMMENDED)

jsx
function ProductCard({ name, price, image }) {
  return (
    <div className='card'>
      <img
        src={image}
        alt={name || 'Product'}
      />
      <h3>{name?.toUpperCase() || 'Unnamed'}</h3>
      <p>${price?.toFixed(2) || 'N/A'}</p>
    </div>
  );
}

🎯 Best Practice: Luôn validate props hoặc dùng default values!


✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)

Knowledge Check

  • [ ] Tôi hiểu Component là gì và cách tạo function component
  • [ ] Tôi biết Props là gì và cách truyền props
  • [ ] Tôi nắm vững props flow một chiều (parent → child)
  • [ ] Tôi thành thạo props destructuring
  • [ ] Tôi hiểu children prop và khi nào dùng
  • [ ] Tôi biết props là read-only, không được sửa
  • [ ] Tôi biết component tên phải viết hoa (PascalCase)
  • [ ] Tôi biết cách handle missing props (default values)
  • [ ] Tôi phân biệt được khi nào nên tách component

Code Review Checklist

Khi viết component, check:

  • [ ] Naming: PascalCase cho component name
  • [ ] Props: Destructuring trong params (hoặc có lý do dùng props object)
  • [ ] Default values: Props optional có default values
  • [ ] Single responsibility: Component chỉ làm 1 việc
  • [ ] Reusability: Component có thể dùng lại không?
  • [ ] Edge cases: Handle missing/invalid props
  • [ ] Performance: Không tạo functions/objects mới trong render (sẽ học sau)
  • [ ] Accessibility: Semantic HTML, alt text, labels

🏠 BÀI TẬP VỀ NHÀ

Bắt buộc (30 phút)

1. Alert Component

Tạo Alert component với props: type, message, onClose.

  • Types: success, warning, error, info
  • Màu sắc khác nhau cho mỗi type
  • Có icon phù hợp (emoji hoặc text)
  • Button close (log to console khi click)
jsx
// Expected usage:
<Alert type="success" message="Profile updated!" onClose={() => {}} />
<Alert type="error" message="Something went wrong" onClose={() => {}} />
💡 Solution
jsx
// Bài tập về nhà - Bài 1: Alert Component
// Tạo component Alert với các tính năng:
// - 4 loại: success, warning, error, info
// - Màu sắc + icon khác nhau cho từng type
// - Nút đóng (close button) → log khi click
// - Dễ mở rộng (dismiss animation, auto-hide sau này)

import React from 'react';

// =============================================
// Alert Component
// =============================================
function Alert({ type = 'info', message, onClose }) {
  // Mapping style & icon theo type
  const alertStyles = {
    success: {
      background: '#ecfdf5',
      borderLeft: '4px solid #10b981',
      color: '#065f46',
      icon: '✅',
    },
    warning: {
      background: '#fffbeb',
      borderLeft: '4px solid #f59e0b',
      color: '#92400e',
      icon: '⚠️',
    },
    error: {
      background: '#fef2f2',
      borderLeft: '4px solid #ef4444',
      color: '#991b1b',
      icon: '❌',
    },
    info: {
      background: '#eff6ff',
      borderLeft: '4px solid #3b82f6',
      color: '#1e40af',
      icon: 'ℹ️',
    },
  };

  // Lấy style theo type (fallback về info nếu type không hợp lệ)
  const style = alertStyles[type] || alertStyles.info;

  const handleClose = () => {
    console.log(`Alert closed: [${type.toUpperCase()}] ${message}`);
    // Trong production: gọi onClose() để parent xử lý state
    if (onClose) onClose();
  };

  return (
    <div
      role='alert'
      className='alert'
      style={{
        position: 'relative',
        padding: '16px 20px 16px 24px',
        marginBottom: '16px',
        borderRadius: '6px',
        backgroundColor: style.background,
        borderLeft: style.borderLeft,
        color: style.color,
        boxShadow: '0 2px 4px rgba(0,0,0,0.05)',
        display: 'flex',
        alignItems: 'flex-start',
        gap: '12px',
        fontFamily: 'system-ui, sans-serif',
      }}
    >
      {/* Icon */}
      <span
        style={{
          fontSize: '1.4rem',
          lineHeight: 1,
          marginTop: '2px',
        }}
      >
        {style.icon}
      </span>

      {/* Nội dung */}
      <div style={{ flex: 1 }}>
        <strong style={{ display: 'block', marginBottom: '4px' }}>
          {type.charAt(0).toUpperCase() + type.slice(1)}
        </strong>
        <span>{message}</span>
      </div>

      {/* Nút đóng */}
      {onClose && (
        <button
          onClick={handleClose}
          aria-label='Đóng thông báo'
          style={{
            background: 'none',
            border: 'none',
            fontSize: '1.4rem',
            cursor: 'pointer',
            color: style.color,
            opacity: 0.7,
            padding: '4px',
            borderRadius: '4px',
            transition: 'opacity 0.2s',
          }}
          onMouseEnter={(e) => (e.target.style.opacity = 1)}
          onMouseLeave={(e) => (e.target.style.opacity = 0.7)}
        >
          ×
        </button>
      )}
    </div>
  );
}

// =============================================
// Demo / Test trong App
// =============================================
function App() {
  return (
    <div
      style={{
        maxWidth: '600px',
        margin: '40px auto',
        padding: '20px',
        fontFamily: 'system-ui, sans-serif',
      }}
    >
      <h2 style={{ textAlign: 'center', marginBottom: '32px' }}>
        Alert Component Demo
      </h2>

      <Alert
        type='success'
        message='Profile updated successfully!'
        onClose={() => console.log('Success alert closed')}
      />

      <Alert
        type='warning'
        message='Your session will expire in 5 minutes.'
        onClose={() => console.log('Warning alert closed')}
      />

      <Alert
        type='error'
        message='Failed to save changes. Please try again.'
        onClose={() => console.log('Error alert closed')}
      />

      <Alert
        type='info'
        message='New feature available in settings.'
        onClose={() => console.log('Info alert closed')}
      />

      {/* Alert không có nút close */}
      <Alert
        type='success'
        message='This alert cannot be closed (no onClose prop)'
      />
    </div>
  );
}

export default App;

/* 
  CSS gợi ý (nếu bạn muốn tách ra file riêng thay vì inline)

  .alert {
    position: relative;
    padding: 16px 20px 16px 24px;
    margin-bottom: 16px;
    border-radius: 6px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
    display: flex;
    align-items: flex-start;
    gap: 12px;
  }

  .alert.success { background: #ecfdf5; border-left: 4px solid #10b981; color: #065f46; }
  .alert.warning { background: #fffbeb; border-left: 4px solid #f59e0b; color: #92400e; }
  .alert.error   { background: #fef2f2; border-left: 4px solid #ef4444; color: #991b1b; }
  .alert.info    { background: #eff6ff; border-left: 4px solid #3b82f6; color: #1e40af; }

  .alert button {
    background: none;
    border: none;
    font-size: 1.4rem;
    cursor: pointer;
    padding: 4px;
    border-radius: 4px;
    transition: opacity 0.2s;
  }

  .alert button:hover { opacity: 1; }
*/

Ghi chú quan trọng:

  • Props linh hoạt: type, message, onClose (optional)
  • Icon emoji thay vì SVG → nhẹ, dễ dùng
  • Accessibility:
    • role="alert"
    • aria-label cho nút close
  • Hover effect trên nút close → UX tốt hơn
  • Dễ mở rộng:
    • Thêm autoClose sau N giây
    • Thêm animation fade-out khi close
    • Thêm icon prop tùy chỉnh

Alert là component xuất hiện ở hầu hết mọi dự án.
Nếu muốn nâng cao, bạn có thể thử thêm:

  • Animation khi xuất hiện/biến mất
  • Hỗ trợ HTML trong message (dùng dangerouslySetInnerHTML)
  • Auto-hide sau 5 giây

2. Profile Card

Tạo ProfileCard component hiển thị thông tin user:

  • Avatar (circular)
  • Name (bold)
  • Title/Role
  • Bio (short description)
  • Social links (Twitter, LinkedIn, GitHub)

Handle edge cases:

  • Missing avatar → default avatar
  • No bio → hide section
  • No social links → hide section
💡 Solution
jsx
// Bài tập về nhà - Bài 2: ProfileCard Component
// Hiển thị thông tin user với:
// - Avatar tròn (circular)
// - Tên in đậm
// - Title/Role
// - Bio (ẩn nếu không có)
// - Social links (Twitter, LinkedIn, GitHub) - ẩn nếu không có
// Xử lý edge cases: avatar thiếu → default, bio/social thiếu → ẩn

import React from 'react';

// =============================================
// ProfileCard Component
// =============================================
function ProfileCard({
  name,
  title = 'Developer',
  avatar,
  bio,
  social = {}, // object chứa các link: { twitter, linkedin, github }
}) {
  // Default avatar nếu không truyền hoặc truyền rỗng
  const defaultAvatar =
    'https://ui-avatars.com/api/?name=' +
    encodeURIComponent(name || 'User') +
    '&background=0D8ABC&color=fff&size=128';

  const displayAvatar = avatar && avatar.trim() ? avatar : defaultAvatar;

  // Kiểm tra xem có social links nào không
  const hasSocial =
    social && (social.twitter || social.linkedin || social.github);

  return (
    <div
      style={{
        width: '320px',
        backgroundColor: 'white',
        borderRadius: '12px',
        boxShadow: '0 10px 25px rgba(0,0,0,0.08)',
        overflow: 'hidden',
        fontFamily: 'system-ui, -apple-system, sans-serif',
        transition: 'transform 0.2s ease',
      }}
      className='profile-card'
      onMouseEnter={(e) => {
        e.currentTarget.style.transform = 'translateY(-6px)';
      }}
      onMouseLeave={(e) => {
        e.currentTarget.style.transform = 'translateY(0)';
      }}
    >
      {/* Header - Avatar & Name */}
      <div
        style={{
          padding: '32px 24px 20px',
          textAlign: 'center',
          background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
          color: 'white',
        }}
      >
        <img
          src={displayAvatar}
          alt={`${name || 'User'}'s avatar`}
          style={{
            width: '96px',
            height: '96px',
            borderRadius: '50%',
            border: '4px solid white',
            objectFit: 'cover',
            boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
            marginBottom: '12px',
          }}
        />
        <h2
          style={{
            margin: '0 0 4px 0',
            fontSize: '1.5rem',
            fontWeight: 700,
          }}
        >
          {name || 'Anonymous User'}
        </h2>
        <p
          style={{
            margin: 0,
            fontSize: '0.95rem',
            opacity: 0.9,
          }}
        >
          {title}
        </p>
      </div>

      {/* Body - Bio & Social */}
      <div style={{ padding: '24px' }}>
        {/* Bio - chỉ hiển thị nếu có */}
        {bio && bio.trim() && (
          <div style={{ marginBottom: '24px' }}>
            <h3
              style={{
                margin: '0 0 8px 0',
                fontSize: '1rem',
                color: '#4b5563',
                fontWeight: 600,
              }}
            >
              About
            </h3>
            <p
              style={{
                margin: 0,
                color: '#374151',
                lineHeight: 1.6,
                fontSize: '0.95rem',
              }}
            >
              {bio}
            </p>
          </div>
        )}

        {/* Social Links - chỉ hiển thị nếu có ít nhất 1 link */}
        {hasSocial && (
          <div>
            <h3
              style={{
                margin: '0 0 12px 0',
                fontSize: '1rem',
                color: '#4b5563',
                fontWeight: 600,
              }}
            >
              Connect
            </h3>
            <div
              style={{
                display: 'flex',
                gap: '16px',
                justifyContent: 'center',
              }}
            >
              {social.twitter && (
                <a
                  href={social.twitter}
                  target='_blank'
                  rel='noopener noreferrer'
                  aria-label='Twitter'
                  style={{
                    color: '#1da1f2',
                    fontSize: '1.6rem',
                    textDecoration: 'none',
                    transition: 'transform 0.2s',
                  }}
                  onMouseEnter={(e) =>
                    (e.currentTarget.style.transform = 'scale(1.2)')
                  }
                  onMouseLeave={(e) =>
                    (e.currentTarget.style.transform = 'scale(1)')
                  }
                >
                  𝕏
                </a>
              )}

              {social.linkedin && (
                <a
                  href={social.linkedin}
                  target='_blank'
                  rel='noopener noreferrer'
                  aria-label='LinkedIn'
                  style={{
                    color: '#0a66c2',
                    fontSize: '1.6rem',
                    textDecoration: 'none',
                    transition: 'transform 0.2s',
                  }}
                  onMouseEnter={(e) =>
                    (e.currentTarget.style.transform = 'scale(1.2)')
                  }
                  onMouseLeave={(e) =>
                    (e.currentTarget.style.transform = 'scale(1)')
                  }
                >
                  in
                </a>
              )}

              {social.github && (
                <a
                  href={social.github}
                  target='_blank'
                  rel='noopener noreferrer'
                  aria-label='GitHub'
                  style={{
                    color: '#24292e',
                    fontSize: '1.6rem',
                    textDecoration: 'none',
                    transition: 'transform 0.2s',
                  }}
                  onMouseEnter={(e) =>
                    (e.currentTarget.style.transform = 'scale(1.2)')
                  }
                  onMouseLeave={(e) =>
                    (e.currentTarget.style.transform = 'scale(1)')
                  }
                >
                  GitHub
                </a>
              )}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// =============================================
// Demo / Test trong App
// =============================================
function App() {
  return (
    <div
      style={{
        minHeight: '100vh',
        background: '#f3f4f6',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        padding: '40px 20px',
        gap: '40px',
        flexWrap: 'wrap',
      }}
    >
      {/* Case 1: Full thông tin */}
      <ProfileCard
        name='Nguyễn Văn Tuân'
        title='Full-stack Developer'
        avatar='https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150'
        bio='Đam mê React, TypeScript và clean code. Hiện đang học cách xây dựng hệ thống lớn tại TP.HCM.'
        social={{
          twitter: 'https://twitter.com/tuan_dev',
          linkedin: 'https://linkedin.com/in/tuandev',
          github: 'https://github.com/tuandevjs',
        }}
      />

      {/* Case 2: Thiếu avatar → dùng default */}
      <ProfileCard
        name='Lê Thị Mai'
        title='UI/UX Designer'
        bio='Thiết kế giao diện đẹp và trải nghiệm người dùng mượt mà.'
        social={{
          linkedin: 'https://linkedin.com/in/maidesign',
        }}
      />

      {/* Case 3: Thiếu bio & social */}
      <ProfileCard
        name='Trần Minh Hoàng'
        title='DevOps Engineer'
        avatar='https://i.pravatar.cc/150?img=68'
      />

      {/* Case 4: Tên dài + không title */}
      <ProfileCard
        name='Phạm Thị Hồng Ngọc Ánh Dương'
        bio='Không có bio ngắn gọn vì cuộc đời tôi rất dài...'
      />
    </div>
  );
}

export default App;

/* 
  Một số cải tiến có thể thêm sau:
  - Thêm nút "Follow" hoặc "Message"
  - Hiệu ứng hover cho avatar (scale lên)
  - Dark mode support
  - Loading state khi avatar đang tải
*/

Các edge cases đã xử lý:

  • Không có avatar → dùng UI Avatars tự động tạo từ tên
  • Không có bio → toàn bộ phần "About" bị ẩn
  • Không có social links → phần "Connect" bị ẩn
  • Tên dài → vẫn hiển thị bình thường (có thể thêm truncate nếu muốn)
  • Title không bắt buộc → mặc định là "Developer"

Điểm nổi bật:

  • Thiết kế hiện đại, gradient header
  • Hover effect nhẹ trên card + social icons
  • Màu sắc hài hòa, dễ đọc
  • Responsive (card tự co lại trên mobile)

Profile card xuất hiện ở hầu hết các trang cá nhân, dashboard, team page...

Nếu muốn nâng cao hơn, bạn có thể thử:

  • Thêm nút "Follow" + trạng thái đã follow/chưa
  • Hiển thị số followers/following
  • Thêm online status dot (xanh/vàng/xám)

Nâng cao (60 phút)

3. Feature Comparison Table

Tạo comparison table cho 3 pricing plans (giống trang pricing của SaaS products).

Components cần:

  • PricingTable (container)
  • PricingColumn (mỗi plan)
  • FeatureRow (mỗi feature)

Props structure:

javascript
const plans = [
  {
    name: 'Basic',
    price: 9,
    features: [
      { name: 'Users', value: '5' },
      { name: 'Storage', value: '10GB' },
      { name: 'Support', value: 'Email' },
    ],
  },
  // ...
];

Yêu cầu:

  • Responsive (stack trên mobile)
  • Highlight "Popular" plan
  • Check/Cross icons cho features
  • CTA button cho mỗi plan
💡 Solution
jsx
// Bài tập về nhà Nâng cao - Bài 3: Pricing / Feature Comparison Table
// Tạo bảng so sánh 3 gói dịch vụ (giống trang pricing của SaaS)
// - Responsive: stack các cột trên mobile
// - Highlight gói "Popular"
// - Check (✓) / Cross (✗) icon cho feature
// - Nút CTA (Call-to-Action) cho mỗi plan

import React from 'react';

// =============================================
// PricingTable - Container chính
// =============================================
function PricingTable({ plans }) {
  // Tìm plan được đánh dấu "popular" (nếu có)
  const popularPlan = plans.find((plan) => plan.popular);

  return (
    <div
      style={{
        maxWidth: '1200px',
        margin: '0 auto',
        padding: '40px 20px',
        fontFamily: 'system-ui, sans-serif',
      }}
    >
      <h1
        style={{
          textAlign: 'center',
          marginBottom: '48px',
          fontSize: '2.5rem',
          color: '#1f2937',
        }}
      >
        Chọn Gói Phù Hợp Với Bạn
      </h1>

      <div
        className='pricing-columns'
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
          gap: '32px',
          alignItems: 'stretch',
        }}
      >
        {plans.map((plan, index) => (
          <PricingColumn
            key={plan.name}
            plan={plan}
            isPopular={plan.popular}
            isFirst={index === 0}
            isLast={index === plans.length - 1}
          />
        ))}
      </div>
    </div>
  );
}

// =============================================
// PricingColumn - Mỗi cột (mỗi gói)
// =============================================
function PricingColumn({ plan, isPopular, isFirst, isLast }) {
  return (
    <div
      style={{
        border: isPopular ? '2px solid #3b82f6' : '1px solid #e5e7eb',
        borderRadius: '16px',
        overflow: 'hidden',
        background: 'white',
        boxShadow: isPopular
          ? '0 20px 35px rgba(59, 130, 246, 0.15)'
          : '0 10px 25px rgba(0,0,0,0.05)',
        position: 'relative',
        transform: isPopular ? 'scale(1.05)' : 'scale(1)',
        zIndex: isPopular ? 10 : 1,
        transition: 'all 0.3s ease',
      }}
    >
      {/* Popular badge */}
      {isPopular && (
        <div
          style={{
            position: 'absolute',
            top: '-14px',
            left: '50%',
            transform: 'translateX(-50%)',
            background: '#3b82f6',
            color: 'white',
            padding: '6px 20px',
            borderRadius: '999px',
            fontSize: '0.9rem',
            fontWeight: '600',
            whiteSpace: 'nowrap',
          }}
        >
          Phổ Biến Nhất
        </div>
      )}

      {/* Header */}
      <div
        style={{
          padding: '32px 24px',
          textAlign: 'center',
          background: isPopular
            ? 'linear-gradient(135deg, #3b82f6, #2563eb)'
            : '#f8f9fa',
          color: isPopular ? 'white' : '#1f2937',
        }}
      >
        <h2
          style={{
            margin: '0 0 8px 0',
            fontSize: '1.8rem',
            fontWeight: '700',
          }}
        >
          {plan.name}
        </h2>

        <div
          style={{
            display: 'flex',
            alignItems: 'baseline',
            justifyContent: 'center',
            gap: '8px',
            margin: '16px 0',
          }}
        >
          <span
            style={{
              fontSize: '2.8rem',
              fontWeight: '800',
            }}
          >
            ${plan.price}
          </span>
          <span
            style={{
              fontSize: '1.1rem',
              opacity: isPopular ? 0.9 : 0.7,
            }}
          >
            /tháng
          </span>
        </div>

        <p
          style={{
            margin: '0',
            fontSize: '1rem',
            opacity: isPopular ? 0.9 : 0.8,
          }}
        >
          {plan.description || 'Phù hợp cho cá nhân và đội nhóm nhỏ'}
        </p>
      </div>

      {/* Features */}
      <div style={{ padding: '32px 24px' }}>
        {plan.features.map((feature, idx) => (
          <FeatureRow
            key={idx}
            feature={feature}
          />
        ))}
      </div>

      {/* CTA Button */}
      <div
        style={{
          padding: '24px',
          borderTop: '1px solid #e5e7eb',
        }}
      >
        <button
          style={{
            width: '100%',
            padding: '16px',
            fontSize: '1.1rem',
            fontWeight: '600',
            color: isPopular ? 'white' : '#3b82f6',
            background: isPopular ? '#3b82f6' : 'white',
            border: isPopular ? 'none' : '2px solid #3b82f6',
            borderRadius: '10px',
            cursor: 'pointer',
            transition: 'all 0.2s ease',
          }}
          onMouseEnter={(e) => {
            e.currentTarget.style.transform = 'scale(1.03)';
            e.currentTarget.style.boxShadow = '0 8px 20px rgba(59,130,246,0.2)';
          }}
          onMouseLeave={(e) => {
            e.currentTarget.style.transform = 'scale(1)';
            e.currentTarget.style.boxShadow = 'none';
          }}
        >
          {plan.cta || 'Chọn gói này'}
        </button>
      </div>
    </div>
  );
}

// =============================================
// FeatureRow - Mỗi dòng tính năng
// =============================================
function FeatureRow({ feature }) {
  const isAvailable = feature.included !== false && feature.value !== false;
  const displayValue = feature.value || (isAvailable ? '✓' : '✗');

  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        padding: '12px 0',
        borderBottom: '1px solid #f0f0f0',
        fontSize: '1rem',
        color: '#374151',
      }}
    >
      <span>{feature.name}</span>

      <span
        style={{
          fontWeight: '600',
          color: isAvailable ? '#10b981' : '#ef4444',
          fontSize:
            displayValue === '✓' || displayValue === '✗' ? '1.3rem' : '1rem',
        }}
      >
        {displayValue}
      </span>
    </div>
  );
}

// =============================================
// Demo / Test trong App
// =============================================
function App() {
  const plans = [
    {
      name: 'Basic',
      price: 9,
      popular: false,
      description: 'Phù hợp cho cá nhân và dự án nhỏ',
      cta: 'Bắt đầu miễn phí',
      features: [
        { name: 'Số người dùng', value: '5' },
        { name: 'Dung lượng lưu trữ', value: '10GB' },
        { name: 'Hỗ trợ', value: 'Email' },
        { name: 'Báo cáo nâng cao', included: false },
        { name: 'Tích hợp API', included: false },
        { name: 'Hỗ trợ ưu tiên', included: false },
      ],
    },
    {
      name: 'Pro',
      price: 29,
      popular: true,
      description: 'Lựa chọn phổ biến nhất cho đội nhóm',
      cta: 'Thử 14 ngày miễn phí',
      features: [
        { name: 'Số người dùng', value: 'Không giới hạn' },
        { name: 'Dung lượng lưu trữ', value: '100GB' },
        { name: 'Hỗ trợ', value: 'Email & Chat' },
        { name: 'Báo cáo nâng cao', included: true },
        { name: 'Tích hợp API', included: true },
        { name: 'Hỗ trợ ưu tiên', included: false },
      ],
    },
    {
      name: 'Enterprise',
      price: 99,
      popular: false,
      description: 'Dành cho doanh nghiệp lớn',
      cta: 'Liên hệ để báo giá',
      features: [
        { name: 'Số người dùng', value: 'Không giới hạn' },
        { name: 'Dung lượng lưu trữ', value: 'Không giới hạn' },
        { name: 'Hỗ trợ', value: '24/7 Phone + On-site' },
        { name: 'Báo cáo nâng cao', included: true },
        { name: 'Tích hợp API', included: true },
        { name: 'Hỗ trợ ưu tiên', included: true },
      ],
    },
  ];

  return <PricingTable plans={plans} />;
}

export default App;

/* 
  Responsive CSS bổ sung (dán vào file CSS hoặc <style> tag)
  @media (max-width: 768px) {
    .pricing-columns {
      grid-template-columns: 1fr !important;
    }
    
    .pricing-columns > div {
      transform: none !important;
      box-shadow: 0 4px 12px rgba(0,0,0,0.08) !important;
    }
  }
*/

Các điểm nổi bật đã xử lý:

  • Responsive: các cột tự động stack trên mobile (grid + minmax)
  • Highlight "Popular": border, scale, badge, shadow mạnh hơn
  • Check/Cross: dùng ✓ (xanh) và ✗ (đỏ) cho feature có/không có
  • CTA button: khác màu + hover effect cho gói Popular
  • Dữ liệu linh hoạt: dễ thêm/xóa feature, plan
  • Accessibility: semantic div + text rõ ràng

Edge cases đã xử lý:

  • Plan không có popular → không highlight
  • Feature không có value → dùng ✓/✗ dựa trên included
  • Không có description → dùng fallback text
  • Mobile → cột tự động xếp dọc

Hầu hết các SaaS đều có pricing table kiểu này.

Nếu muốn thử thách thêm, bạn có thể:

  • Thêm animation khi hover package
  • Thêm "Most Popular" badge xoay nhẹ
  • Làm nút CTA có loading state khi click
  • Hỗ trợ dark mode (thay đổi màu khi theme thay đổi)

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Official Docs - Components and Propshttps://react.dev/learn/passing-props-to-a-component
  2. MDN - Destructuring Assignmenthttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

Đọc thêm

  1. React Docs - Passing JSX as childrenhttps://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children

  2. Kent C. Dodds - React Component Patternshttps://kentcdodds.com/blog/react-component-patterns


🔗 KẾT NỐI KIẾN THỨC

Kiến thức nền (Đã học)

  • Ngày 1-2: JavaScript ES6+ (destructuring, spread, arrow functions)
  • Ngày 3: JSX syntax, expressions trong JSX

Hướng tới (Sẽ học)

  • Ngày 5: Events & Conditional Rendering (dùng props để trigger events)
  • Ngày 6: Lists & Keys (render nhiều components với props)
  • Ngày 7: Component Composition (advanced children patterns)
  • Ngày 11: useState (kết hợp props + state)

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Props Validation:

jsx
// Development: PropTypes (runtime)
import PropTypes from 'prop-types';

ProductCard.propTypes = {
  name: PropTypes.string.isRequired,
  price: PropTypes.number.isRequired,
  image: PropTypes.string,
};

// Production: TypeScript (compile-time)
interface ProductCardProps {
  name: string;
  price: number;
  image?: string;
}

2. Performance:

jsx
// ❌ BAD: Tạo object mới mỗi render
<Component style={{ margin: 10 }} />

// ✅ GOOD: Extract ra constant
const styles = { margin: 10 };
<Component style={styles} />

// Hoặc
<Component className="my-class" />

3. Component Size:

  • Component nên < 200 lines
  • Nếu > 200 lines → refactor thành smaller components
  • Single Responsibility Principle

Câu Hỏi Phỏng Vấn

Junior Level:

Q1: "Props và State khác nhau như thế nào?"
A: Props được truyền từ parent, read-only. State được quản lý trong component, có thể thay đổi. (State sẽ học ngày 11)

Q2: "Children prop là gì?"
A: Children là prop đặc biệt chứa content bên trong component tags. Dùng cho wrapper components.

Mid Level:

Q3: "Làm thế nào để truyền data từ child lên parent?"
A: Parent truyền callback function qua props. Child gọi callback với data. (Sẽ practice ngày 5 - Events)

Senior Level:

Q4: "Component composition vs inheritance trong React?"
A: React khuyến khích composition (dùng children, props) thay vì inheritance. Flexible và maintainable hơn.


War Stories

Story 1: Props Mutation Bug

"Năm 2019, team mình có bug nghiêm trọng: checkout cart bị reset random. Root cause: Junior developer sửa props.cartItems.push(newItem) thay vì tạo array mới. Mất 2 ngày debug vì lỗi chỉ xảy ra khi user click nhanh. Lesson: Luôn treat props như immutable!"

Story 2: Component Naming

"Một lần review code, thấy component tên button. Hỏi why lowercase? Dev: 'Em nghĩ button là HTML tag nên viết thường'. QA report button không hoạt động. Fix: đổi tên Button. Nhớ đời: Component = PascalCase!"


🎯 PREVIEW NGÀY MAI

Ngày 5: Events & Conditional Rendering

Tomorrow chúng ta sẽ học:

  • Event handling trong React
  • Synthetic events
  • Event binding patterns
  • Conditional rendering (if, &&, ternary)

Chuẩn bị:

  • Review Ngày 4 (Props) - sẽ dùng props để pass event handlers
  • Ôn lại ternary operator: condition ? true : false
  • Nghĩ về các UI elements có nhiều states (loading, error, success)

Sneak peek:

jsx
// Tomorrow sẽ học pattern này:
<Button
  text='Click me'
  onClick={() => console.log('Clicked!')}
  disabled={isLoading}
/>;

{
  isLoading ? <Spinner /> : <Content />;
}

🎊 CHÚC MỪNG!

Bạn đã hoàn thành Ngày 4: Components & Props!

Hôm nay bạn đã học: ✅ Function Components & Props flow
✅ Props destructuring & default values
✅ Children prop & composition
✅ Real-world component patterns
✅ Production best practices

Next steps:

  1. Hoàn thành bài tập về nhà
  2. Review các debug labs
  3. Practice tạo ít nhất 3 components mới
  4. Chuẩn bị cho Ngày 5: Events!

Personal tech knowledge base