Skip to content

📅 NGÀY 5: EVENTS & CONDITIONAL RENDERING - TƯƠNG TÁC VÀ LOGIC HIỂN THỊ

🎯 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õ Event handling trong React và sự khác biệt với vanilla JavaScript
  • [ ] Nắm vững Synthetic Events và tại sao React dùng nó
  • [ ] Thành thạo các Event binding patterns (inline, arrow, method)
  • [ ] Sử dụng thành thạo Conditional rendering với if/else, &&, ternary operator
  • [ ] Phân biệt được khi nào dùng pattern nào cho conditional rendering

🤔 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. Props là gì? Nó read-only hay có thể modify?
  2. Arrow function syntax như thế nào? () => {} khác gì với function() {}?
  3. Ternary operator hoạt động ra sao? Cho ví dụ.
💡 Solution
  1. Props là data truyền từ parent → child. Props là read-only (immutable), child không được sửa props.

  2. Arrow function:

javascript
// Arrow function
const add = (a, b) => a + b;

// Regular function
function add(a, b) {
  return a + b;
}

// Khác biệt chính: Arrow function không có 'this' binding riêng
  1. Ternary operator:
javascript
const age = 18;
const canVote = age >= 18 ? 'Yes' : 'No';

// Equivalent to:
let canVote;
if (age >= 18) {
  canVote = 'Yes';
} else {
  canVote = 'No';
}

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

1.1 Vấn Đề Thực Tế

Bạn đang xây dựng một nút "Like" trên Facebook. Cần xử lý:

html
<!-- Vanilla JavaScript -->
<button id="likeBtn">Like</button>

<script>
  const btn = document.getElementById('likeBtn');
  btn.addEventListener('click', function () {
    console.log('Liked!');
  });
</script>

Vấn đề với vanilla JS:

  • 🔴 Phải query DOM (getElementById) → Lập trình viên phải tự tìm và quản lý từng phần tử DOM thủ công, code dài dòng, dễ sai khi DOM thay đổi.

  • 🔴 Attach/detach listeners manually → Phải tự addEventListenerremoveEventListener, dễ quên cleanup khi component không còn dùng.

  • 🔴 Memory leaks nếu quên cleanup → Event listener hoặc reference còn tồn tại trong bộ nhớ dù element đã bị remove, gây tốn RAM và giảm hiệu năng.

  • 🔴 Cross-browser compatibility issues → Một số API hoặc hành vi JS khác nhau giữa các trình duyệt, cần polyfill hoặc workaround phức tạp.

  • 🔴 Event delegation phức tạp → Khi có nhiều element động, phải tự xử lý bubbling/capturing, code khó đọc và khó bảo trì.

1.2 Giải Pháp: React Events

React xử lý events declaratively - bạn chỉ cần khai báo, React lo phần còn lại.

jsx
// React way - Declarative
function LikeButton() {
  const handleClick = () => {
    console.log('Liked!');
  };

  return <button onClick={handleClick}>Like</button>;
}

// ✅ No DOM queries
// ✅ No manual addEventListener
// ✅ Auto cleanup
// ✅ Cross-browser compatible

Lợi ích:

  • ✅ Declarative (khai báo ý định, không lo implementation)
  • ✅ Auto cleanup khi component unmount
  • ✅ Consistent API across browsers
  • ✅ Performance optimization (event delegation)

1.3 Mental Model

┌─────────────────────────────────────────────────┐
│           VANILLA JAVASCRIPT                    │
│  ┌──────────────────────────────────────┐       │
│  │  1. Query DOM                        │       │
│  │  const btn = document.getElementById │       │
│  │                                      │       │
│  │  2. Attach Listener                  │       │
│  │  btn.addEventListener('click', fn)   │       │
│  │                                      │       │
│  │  3. Handle Event                     │       │
│  │  function fn(event) { ... }          │       │
│  │                                      │       │
│  │  4. Cleanup (often forgotten!)       │       │
│  │  btn.removeEventListener('click',fn) │       │
│  └──────────────────────────────────────┘       │
└─────────────────────────────────────────────────┘

                      VS

┌─────────────────────────────────────────────────┐
│                 REACT                           │
│  ┌──────────────────────────────────────┐       │
│  │  <button onClick={handleClick}>      │       │
│  │                                      │       │
│  │  React handles:                      │       │
│  │  ✅ Event delegation                 │       │
│  │  ✅ Cross-browser normalization      │       │
│  │  ✅ Auto cleanup                     │       │
│  │  ✅ Performance optimization         │       │
│  └──────────────────────────────────────┘       │
└─────────────────────────────────────────────────┘

🔑 Synthetic Events

React không sử dụng trực tiếp native browser events. Thay vào đó, React bọc (wrap) chúng trong SyntheticEvent để chuẩn hóa hành vi và cung cấp một API thống nhất cho lập trình viên.

Browser Event (native)

  React intercepts

  SyntheticEvent (normalized)

  Your handler function

Khi bạn dùng onClick, onChange, onSubmit…, tham số e nhận được là SyntheticEvent (wrapper), không phải event gốc của trình duyệt.


Hình dung đơn giản:

Click chuột thật (trình duyệt)

 Browser tạo MouseEvent

 React bọc lại

 SyntheticEvent

 onClick={(e) => { ... }}

So sánh trực quan:

Native event (vanilla JS):

js
button.addEventListener('click', (e) => {
  console.log(e instanceof MouseEvent); // true
});

SyntheticEvent (React):

jsx
<button onClick={(e) => {
  console.log(e); // SyntheticEvent
}}>

e không phải MouseEvent gốc, mà là object event do React quản lý.


Tại sao React dùng SyntheticEvent?

  • 🟢 Consistent API across browsers → Chuẩn hóa sự khác biệt giữa các trình duyệt, dev chỉ làm việc với một API thống nhất.

  • 🟢 Kiểm soát kiến trúc event → React intercept và phân phối event thông qua event system riêng.

  • 🟢 Đơn giản hóa cho lập trình viên → Không cần quan tâm đến chi tiết implementation của từng browser.


Event Pooling & event.persist() (Lịch sử & hiện tại)

  • React ≤16:

    • SyntheticEvent được tái sử dụng (Event Pooling)
    • Sau khi handler chạy xong, các thuộc tính event bị reset về null
    • Muốn dùng event trong async code → bắt buộc gọi event.persist()
  • React 17+:

    • ❌ Event Pooling đã bị loại bỏ
    • event.persist() không còn cần thiết (no-op, chỉ để tương thích ngược)

Cách dùng event.persist() (React 16 trở xuống):

js
const handleChange = (e) => {
  // Giữ event không bị thu hồi về pool
  e.persist();

  setTimeout(() => {
    console.log('Giá trị sau 1s:', e.target.value);
  }, 1000);
};

Nếu không gọi e.persist() trong React 16:

js
setTimeout(() => {
  console.log(e.target.value); // ❌ e.target === null
}, 1000);

Xử lý bất đồng bộ (React 17+):

jsx
<button onClick={(e) => {
  console.log(e.target); // OK

  setTimeout(() => {
    console.log(e.target); // Vẫn OK, không cần e.persist()
  }, 1000);
}}>

→ React để Garbage Collector của trình duyệt tự xử lý object event.


Best Practice (mọi phiên bản React):

js
const handleChange = (e) => {
  const value = e.target.value; // lưu giá trị cần dùng

  setTimeout(() => {
    console.log('Giá trị sau 1s:', value);
  }, 1000);
};

→ Không phụ thuộc vòng đời event, không cần event.persist().


Tóm lại:

  • SyntheticEvent = wrapper có kiểm soát của browser event

  • Mục tiêu chính: cross-browser consistency

  • React ≤16: cần event.persist() khi xử lý async

  • React 17+:

    • Không còn event pooling
    • event.persist() không còn tác dụng
  • Khuyến nghị: copy dữ liệu cần dùng ra biến riêng để code gọn và an toàn

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

Hiểu lầm 1: "onClick giống onclick trong HTML"

jsx
// ❌ HTML - string
<button onclick="handleClick()">Click</button>

// ✅ React - function reference
<button onClick={handleClick}>Click</button>

// ❌ SAI - gọi function ngay lập tức!
<button onClick={handleClick()}>Click</button>

Giải thích:

  • HTML: onclick="string" - execute khi click
  • React: onClick={function} - pass function reference
  • onClick={handleClick()} - execute NGAY, không đợi click!

Hiểu lầm 2: "event.preventDefault() không cần thiết"

jsx
// ❌ SAI - form submit sẽ reload page
function Form() {
  const handleSubmit = (event) => {
    // Missing preventDefault!
    console.log('Submitted');
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

// ✅ ĐÚNG
function Form() {
  const handleSubmit = (event) => {
    event.preventDefault(); // Prevent default behavior
    console.log('Submitted');
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

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

Demo 1: Event Binding Patterns ⭐

Pattern 1: Inline Arrow Function

jsx
function Counter() {
  const handleClick = () => {
    console.log('Clicked!');
  };

  return (
    <div>
      {/* ❌ CÁCH 1: Call function immediately - SAI! */}
      <button onClick={handleClick()}>Wrong - Executes now!</button>

      {/* ✅ CÁCH 2: Pass function reference */}
      <button onClick={handleClick}>Correct - Pass reference</button>

      {/* ✅ CÁCH 3: Inline arrow function */}
      <button onClick={() => handleClick()}>
        Also correct - Arrow wrapper
      </button>
    </div>
  );
}

Pattern 2: Passing Arguments

jsx
function TodoList() {
  const handleDelete = (id) => {
    console.log(`Delete todo ${id}`);
  };

  return (
    <div>
      {/* ❌ SAI - calls immediately */}
      <button onClick={handleDelete(1)}>Delete</button>

      {/* ✅ ĐÚNG - arrow function wrapper */}
      <button onClick={() => handleDelete(1)}>Delete 1</button>
      <button onClick={() => handleDelete(2)}>Delete 2</button>
      <button onClick={() => handleDelete(3)}>Delete 3</button>
    </div>
  );
}

So sánh trade-off:

Cách viếtƯu điểmNhược điểmKhi nên dùng
onClick={handleClick}✅ Gọn gàng
✅ Tối ưu hiệu năng
❌ Không truyền được tham sốHandler đơn giản, không cần tham số
onClick={() => handleClick()}✅ Truyền được tham số
✅ Linh hoạt
❌ Tạo function mới mỗi lần renderKhi cần truyền tham số

💡 Alternative: Higher-Order Function

jsx
function TodoList() {
  // Phương pháp currying : function returning function
  const handleDelete = (id) => (event) => {
    console.log(`Delete todo ${id}`);
  };

  return (
    <div>
      {/* Gọn hơn - không có inline arrow */}
      <button onClick={handleDelete(1)}>Delete 1</button>
      <button onClick={handleDelete(2)}>Delete 2</button>
      <button onClick={handleDelete(3)}>Delete 3</button>
    </div>
  );
}

Demo 2: Event Object & Common Events ⭐⭐

jsx
function EventDemo() {
  // onClick - Mouse click
  const handleClick = (event) => {
    console.log('Event type:', event.type); // 'click'
    console.log('Target element:', event.target); // <button>
    console.log('Mouse position:', event.clientX, event.clientY);
  };

  // onChange - Input change
  const handleChange = (event) => {
    console.log('Input value:', event.target.value);
  };

  // onSubmit - Form submit
  const handleSubmit = (event) => {
    event.preventDefault(); // IMPORTANT!
    console.log('Form submitted');
  };

  // onKeyDown - Keyboard
  const handleKeyDown = (event) => {
    console.log('Key pressed:', event.key);
    if (event.key === 'Enter') {
      console.log('Enter pressed!');
    }
  };

  // onMouseEnter/onMouseLeave - Hover
  const handleMouseEnter = () => {
    console.log('Mouse entered');
  };

  return (
    <div>
      <button onClick={handleClick}>Click Me</button>

      <input
        type='text'
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        placeholder='Type something...'
      />

      <form onSubmit={handleSubmit}>
        <input type='text' />
        <button type='submit'>Submit</button>
      </form>

      <div
        onMouseEnter={handleMouseEnter}
        style={{ padding: 20, background: '#f0f0f0' }}
      >
        Hover over me
      </div>
    </div>
  );
}

🔥 Các sự kiện thường dùng:

Sự kiệnKhi nào được kích hoạtTrường hợp sử dụng
onClickKhi click vào phần tửButton, link
onChangeKhi giá trị input thay đổiForm, input
onSubmitKhi form được submitXử lý form
onKeyDownKhi nhấn phímPhím tắt, shortcut
onKeyUpKhi nhả phímValidate dữ liệu nhập
onFocusKhi phần tử được focusHighlight input
onBlurKhi phần tử mất focusValidate khi blur
onMouseEnterKhi chuột đi vào phần tửHiệu ứng hover
onMouseLeaveKhi chuột rời khỏi phần tửẨn tooltip

Demo 3: Event Delegation (Ủy quyền) & Bubbling (Nổi Bọt) ⭐⭐⭐

jsx
function EventBubbling() {
  const handleParentClick = () => {
    console.log('Parent clicked');
  };

  const handleChildClick = (event) => {
    console.log('Child clicked');
    // event.stopPropagation();
    // Bỏ comment để dừng lan truyền Bubbling lên cha
  };

  const handleButtonClick = (event) => {
    console.log('Button clicked');
    event.stopPropagation(); // Dừng lan truyền / nổi bọt
  };

  return (
    <div
      onClick={handleParentClick}
      style={{ padding: 40, background: '#f0f0f0' }}
    >
      Parent
      <div
        onClick={handleChildClick}
        style={{ padding: 20, background: '#e0e0e0', margin: 10 }}
      >
        Child
        <button onClick={handleButtonClick}>Button (stops propagation)</button>
      </div>
    </div>
  );
}

Luồng sự kiện (Event Flow):

Click button

Handler của button được thực thi

event.stopPropagation()

❌ Không bubble lên component con
❌ Không bubble lên component cha

Khi không dùng stopPropagation:

Click button

Handler của button → 'Button clicked'

Handler của Child → 'Child clicked'

Handler của Parent → 'Parent clicked'

⚠️ Khi nào dùng stopPropagation:

  • ✅ Modal close button (không close khi click content)
  • ✅ Nested clickable elements (Các phần tử có thể nhấp lồng nhau như ví dụ trên)
  • ✅ Prevent parent handlers ( Ngăn việc click ở child nhưng xử lý luôn cả event ở parent )
  • ❌ Đừng overuse (lạm dụng) - phá vỡ event delegation benefits

💻 PHẦN 2B: LIVE CODING - CONDITIONAL RENDERING (45 phút)

Demo 4: If/Else Pattern ⭐

jsx
function UserGreeting({ isLoggedIn, userName }) {
  // ❌ CÁCH 1: Không thể sử dụng câu lệnh if/else trực tiếp trong JSX.
  return (
    <div>
      {if (isLoggedIn) { // ❌ Syntax Error!
        <h1>Welcome, {userName}!</h1>
      } else {
        <h1>Please log in</h1>
      }}
    </div>
  );

  // ✅ CÁCH 2: If/else before return
  if (isLoggedIn) {
    return <h1>Welcome, {userName}!</h1>;
  } else {
    return <h1>Please log in</h1>;
  }

  // ✅ CÁCH 3: Variable + if/else
  let message;
  if (isLoggedIn) {
    message = <h1>Welcome, {userName}!</h1>;
  } else {
    message = <h1>Please log in</h1>;
  }

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

🎯 Khi nào dùng If/Else:

  • ✅ Logic phức tạp (nhiều conditions)
  • ✅ Early return pattern
  • ✅ Nhiều lines code trong mỗi branch
  • ❌ Nếu true/false đơn giản → dùng ternary

Demo 5: Ternary Operator Pattern ⭐⭐

jsx
function StatusBadge({ status }) {
  // ✅ Ternary operator - clean for simple conditions
  return (
    <span
      className={`badge ${
        status === 'active' ? 'badge-success' : 'badge-danger'
      }`}
    >
      {status === 'active' ? '✓ Active' : '✗ Inactive'}
    </span>
  );
}

function LoadingButton({ isLoading, onClick, children }) {
  return (
    <button
      onClick={onClick}
      disabled={isLoading}
    >
      {isLoading ? (
        <>
          <span className='spinner'>⏳</span>
          Loading...
        </>
      ) : (
        children
      )}
    </button>
  );
}

// Usage
function App() {
  return (
    <div>
      <StatusBadge status='active' />
      <StatusBadge status='inactive' />

      <LoadingButton
        isLoading={false}
        onClick={() => {}}
      >
        Submit
      </LoadingButton>

      <LoadingButton
        isLoading={true}
        onClick={() => {}}
      >
        Submit
      </LoadingButton>
    </div>
  );
}

⚠️ Nested Ternary - TRÁNH!

jsx
// ❌ NÊN TRÁNH - Khó đọc
function Status({ user }) {
  return (
    <div>
      {user ? (
        user.isPremium ? (
          user.isActive ? (
            <span>Premium Active</span>
          ) : (
            <span>Premium Inactive</span>
          )
        ) : (
          <span>Free</span>
        )
      ) : (
        <span>Guest</span>
      )}
    </div>
  );
}

// ✅ TỐT HƠN - Sử dụng nhiều câu lệnh if/else hoặc hàm riêng biệt
function Status({ user }) {
  if (!user) return <span>Guest</span>;
  if (!user.isPremium) return <span>Free</span>;
  if (user.isActive) return <span>Premium Active</span>;
  return <span>Premium Inactive</span>;
}

Demo 6: Logical && Operator ⭐⭐⭐

jsx
function Notification({ hasNotification, count }) {
  return (
    <div>
      <h3>Inbox</h3>

      {/* ✅ Chỉ hiển thị khi hasNotification = true */}
      {hasNotification && <div className='notification-badge'>{count}</div>}

      {/* ✅ Chỉ hiển thị khi count > 0 */}
      {count > 0 && <p>You have {count} unread messages</p>}

      {/* ⚠️ CẨN THẬN – có thể render ra số 0 */}
      {count && <p>Count: {count}</p>}
      {/* Nếu count = 0 → sẽ hiển thị "0" trên màn hình */}

      {/* ✅ TỐT HƠN – so sánh rõ ràng */}
      {count > 0 && <p>Count: {count}</p>}
    </div>
  );
}

🔥 Lưu ý: Các giá trị falsy

jsx
function Demo({ value }) {
  return (
    <div>
      {/* ❌ SẼ RENDER "0" nếu value = 0 */}
      {value && <p>Value: {value}</p>}

      {/* ✅ ĐÚNG – chỉ render khi value > 0 */}
      {value > 0 && <p>Value: {value}</p>}

      {/* ✅ ĐÚNG – render với mọi giá trị khác null/undefined */}
      {value != null && <p>Value: {value}</p>}
    </div>
  );
}

// Test cases:
<Demo value={0} />    // Renders: "0" (Ối dồi ôi !)
<Demo value={5} />    // Renders: "Value: 5" ✓
<Demo value={null} /> // Renders: nothing ✓

Falsy values trong JavaScript:

  • false
  • 0 ⚠️ renders as "0"
  • "" (empty string)
  • null
  • undefined
  • NaN

Demo 7: Multiple Conditions Pattern ⭐⭐⭐

jsx
function UserDashboard({ user, isLoading, error }) {
  // Pattern 1: Early returns
  if (isLoading) {
    return <div className='loading'>Loading...</div>;
  }

  if (error) {
    return <div className='error'>Error: {error.message}</div>;
  }

  if (!user) {
    return <div className='empty'>No user data</div>;
  }

  // Main content
  return (
    <div className='dashboard'>
      <h1>Welcome, {user.name}!</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

// Pattern 2: Object mapping
function StatusMessage({ status }) {
  const messages = {
    loading: <div className='spinner'>Loading...</div>,
    success: <div className='success'>✓ Success!</div>,
    error: <div className='error'>✗ Error occurred</div>,
    idle: null,
  };

  return messages[status] || null;
}

// Pattern 3: Switch statement
function OrderStatus({ status }) {
  const renderStatus = () => {
    switch (status) {
      case 'pending':
        return <span className='badge-warning'>⏳ Pending</span>;
      case 'processing':
        return <span className='badge-info'>⚙️ Processing</span>;
      case 'shipped':
        return <span className='badge-primary'>📦 Shipped</span>;
      case 'delivered':
        return <span className='badge-success'>✓ Delivered</span>;
      case 'cancelled':
        return <span className='badge-danger'>✗ Cancelled</span>;
      default:
        return <span className='badge-secondary'>Unknown</span>;
    }
  };

  return <div>{renderStatus()}</div>;
}

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

⭐ Exercise 1: Click Counter (15 phút)

🎯 Mục tiêu: Tạo counter với event handling cơ bản
⏱️ Thời gian: 15 phút
🚫 KHÔNG dùng: State (chưa học - chỉ log ra console)

Requirements:

  1. Button "Increment" - log "Count: 1", "Count: 2"...
  2. Button "Decrement" - log "Count: -1", "Count: -2"...
  3. Button "Reset" - log "Count reset to 0"
  4. Hiển thị message dựa trên count (dùng conditional rendering concept)
jsx
/**
 * 💡 Gợi ý:
 * - Dùng onClick để handle button clicks
 * - Log count value ra console
 * - Conditional rendering để show message
 */

// 🎯 NHIỆM VỤ CỦA BẠN:
function ClickCounter() {
  let count = 0; // Temporary - sẽ dùng state sau

  const handleIncrement = () => {
    count = count + 1;
    console.log(`Count: ${count}`);
  };

  const handleDecrement = () => {
    // TODO
  };

  const handleReset = () => {
    // TODO
  };

  return (
    <div className='counter'>
      <h2>Click Counter (Console Demo)</h2>

      {/* TODO: Add buttons */}
      <button onClick={handleIncrement}>Increment</button>

      {/* TODO: Render theo điều kiện  */}
      {/* If count > 5: "Wow, nhiều clicks!" */}
      {/* If count < 0: "Số âm !" */}
      {/* Else: "Tiếp tục nhấp đi!" */}
    </div>
  );
}
💡 Solution
jsx
function ClickCounter() {
  let count = 0;

  const handleIncrement = () => {
    count++;
    console.log(`Count: ${count}`);
  };

  const handleDecrement = () => {
    count--;
    console.log(`Count: ${count}`);
  };

  const handleReset = () => {
    count = 0;
    console.log('Count reset to 0');
  };

  // Temporary message - won't update visually (need state)
  let message;
  if (count > 5) {
    message = 'Wow, nhiều clicks!';
  } else if (count < 0) {
    message = 'Số âm!';
  } else {
    message = 'Tiếp tục nhấp đi!';
  }

  return (
    <div className='counter'>
      <h2>Click Counter (Console Demo)</h2>
      <p className='hint'>Mở bảng console để xem các cập nhật số liệu.</p>

      <div className='button-group'>
        <button onClick={handleDecrement}>➖ Decrement</button>
        <button onClick={handleReset}>🔄 Reset</button>
        <button onClick={handleIncrement}>➕ Increment</button>
      </div>

      <p className='message'>{message}</p>
    </div>
  );
}

📚 Giải thích:

  • Biến count không được giữ lại giữa các lần click (cần dùng state)
  • Event handler log ra console đúng như mong đợi
  • Conditional rendering hiển thị các thông báo khác nhau
  • Ngày 11 sẽ học useState để cập nhật UI!

⭐⭐ Exercise 2: Form Input Handler (25 phút)

🎯 Mục tiêu: Handle form events & conditional validation
⏱️ Thời gian: 25 phút

Scenario: Tạo login form với real-time validation messages

Requirements:

  1. Email input với onChange validation
  2. Password input với visibility toggle
  3. Submit button disabled nếu invalid
  4. Show error messages conditionally
jsx
// 🎯 NHIỆM VỤ:
function LoginForm() {
  // TODO: Handle email change
  const handleEmailChange = (event) => {
    const email = event.target.value;
    console.log('Email:', email);
    // TODO: Validate email format
  };

  // TODO: Handle password change
  const handlePasswordChange = (event) => {
    // TODO
  };

  // TODO: Handle form submit
  const handleSubmit = (event) => {
    event.preventDefault(); // IMPORTANT!
    console.log('Form submitted');
  };

  // TODO: Toggle password visibility
  const handleTogglePassword = () => {
    // TODO
  };

  // Validation (temporary - need state to track)
  const isEmailValid = true; // TODO: Real validation
  const isPasswordValid = true; // TODO: Min 6 chars
  const canSubmit = isEmailValid && isPasswordValid;

  return (
    <form
      onSubmit={handleSubmit}
      className='login-form'
    >
      <h2>Login</h2>

      {/* Email field */}
      <div className='form-group'>
        <label htmlFor='email'>Email</label>
        <input
          type='email'
          id='email'
          onChange={handleEmailChange}
          placeholder='Enter your email'
        />

        {/* TODO: Show error if email invalid */}
        {!isEmailValid && <span className='error'>Invalid email format</span>}
      </div>

      {/* Password field */}
      <div className='form-group'>
        <label htmlFor='password'>Password</label>
        <div className='password-wrapper'>
          <input
            type='password' // TODO: Toggle between 'password' and 'text'
            id='password'
            onChange={handlePasswordChange}
            placeholder='Enter your password'
          />
          <button
            type='button'
            onClick={handleTogglePassword}
          >
            👁️ Show
          </button>
        </div>

        {/* TODO: Show error if password too short */}
      </div>

      {/* Submit button */}
      <button
        type='submit'
        disabled={!canSubmit}
        className='submit-btn'
      >
        Log In
      </button>
    </form>
  );
}
💡 Solution
jsx
function LoginForm() {
  // Temp variables (need state to persist)
  let email = '';
  let password = '';
  let showPassword = false;

  const handleEmailChange = (event) => {
    email = event.target.value;
    console.log('Email:', email);
  };

  const handlePasswordChange = (event) => {
    password = event.target.value;
    console.log('Password:', password);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('Form submitted');
    console.log('Email:', email);
    console.log('Password:', password);
  };

  const handleTogglePassword = () => {
    showPassword = !showPassword;
    console.log('Show password:', showPassword);
  };

  // Basic validation (without state, always resets)
  const isEmailValid = email.includes('@') && email.includes('.');
  const isPasswordValid = password.length >= 6;
  const canSubmit = email && password && isEmailValid && isPasswordValid;

  return (
    <form
      onSubmit={handleSubmit}
      className='login-form'
    >
      <h2>Login</h2>

      <div className='form-group'>
        <label htmlFor='email'>Email</label>
        <input
          type='email'
          id='email'
          onChange={handleEmailChange}
          placeholder='Enter your email'
        />

        {email && !isEmailValid && (
          <span className='error'>✗ Please enter a valid email address</span>
        )}

        {email && isEmailValid && (
          <span className='success'>✓ Valid email</span>
        )}
      </div>

      <div className='form-group'>
        <label htmlFor='password'>Password</label>
        <div className='password-wrapper'>
          <input
            type={showPassword ? 'text' : 'password'}
            id='password'
            onChange={handlePasswordChange}
            placeholder='Enter your password'
          />
          <button
            type='button'
            onClick={handleTogglePassword}
            className='toggle-btn'
          >
            {showPassword ? '🙈 Hide' : '👁️ Show'}
          </button>
        </div>

        {password && password.length < 6 && (
          <span className='error'>
            ✗ Password must be at least 6 characters
          </span>
        )}

        {password && isPasswordValid && (
          <span className='success'>✓ Strong password</span>
        )}
      </div>

      <button
        type='submit'
        disabled={!canSubmit}
        className='submit-btn'
      >
        {canSubmit ? 'Log In' : 'Fill all fields correctly'}
      </button>
    </form>
  );
}

⭐⭐⭐ Exercise 3: Interactive Todo Item (40 phút)

🎯 Mục tiêu: Kết hợp events + conditional rendering
⏱️ Thời gian: 40 phút

📋 Product Requirements: User Story: "Là user, tôi muốn thấy todo item với các trạng thái khác nhau (view/edit/delete confirm) để quản lý tasks."

✅ Acceptance Criteria:

  • [ ] 3 modes: View, Edit, Delete Confirm
  • [ ] View mode: Show todo, Edit button, Delete button
  • [ ] Edit mode: Input field, Save button, Cancel button
  • [ ] Delete Confirm mode: Confirmation message, Yes/No buttons
  • [ ] Conditional styling cho mỗi mode
jsx
// 🎯 NHIỆM VỤ:
function TodoItem({ todo }) {
  // Modes: 'view' | 'edit' | 'delete-confirm'
  let mode = 'view'; // TODO: Toggle between modes

  const handleEditClick = () => {
    mode = 'edit';
    console.log('Entering edit mode');
  };

  const handleDeleteClick = () => {
    mode = 'delete-confirm';
    console.log('Asking for delete confirmation');
  };

  const handleSave = () => {
    console.log('Saved');
    mode = 'view';
  };

  const handleCancel = () => {
    console.log('Cancelled');
    mode = 'view';
  };

  const handleConfirmDelete = () => {
    console.log(`Deleted todo: ${todo.id}`);
  };

  const handleCancelDelete = () => {
    mode = 'view';
  };

  return (
    <div className='todo-item'>
      {/* TODO: Conditional rendering based on mode */}

      {/* VIEW MODE */}
      {mode === 'view' && (
        <div className='view-mode'>
          <span className='todo-text'>{todo.text}</span>
          <div className='actions'>
            <button onClick={handleEditClick}>✏️ Edit</button>
            <button onClick={handleDeleteClick}>🗑️ Delete</button>
          </div>
        </div>
      )}

      {/* EDIT MODE - TODO */}

      {/* DELETE CONFIRM MODE - TODO */}
    </div>
  );
}

// Test
function App() {
  const todo = { id: 1, text: 'Learn React Events' };
  return <TodoItem todo={todo} />;
}
💡 Solution
jsx
function TodoItem({ todo }) {
  // Simulate mode state (won't persist - need useState)
  let mode = 'view'; // 'view' | 'edit' | 'delete-confirm'
  let editText = todo.text;

  const handleEditClick = () => {
    mode = 'edit';
    console.log('Mode:', mode);
  };

  const handleDeleteClick = () => {
    mode = 'delete-confirm';
    console.log('Mode:', mode);
  };

  const handleInputChange = (event) => {
    editText = event.target.value;
    console.log('Edit text:', editText);
  };

  const handleSave = () => {
    console.log('Saved:', editText);
    mode = 'view';
  };

  const handleCancel = () => {
    console.log('Cancelled edit');
    editText = todo.text;
    mode = 'view';
  };

  const handleConfirmDelete = () => {
    console.log(`Deleted todo: ${todo.id}`);
    // In real app: call parent's delete function
  };

  const handleCancelDelete = () => {
    console.log('Cancelled delete');
    mode = 'view';
  };

  // Conditional rendering based on mode
  return (
    <div className={`todo-item mode-${mode}`}>
      {/* VIEW MODE */}
      {mode === 'view' && (
        <div className='view-mode'>
          <span className='todo-text'>{todo.text}</span>
          <div className='actions'>
            <button
              onClick={handleEditClick}
              className='btn btn-edit'
            >
              ✏️ Edit
            </button>
            <button
              onClick={handleDeleteClick}
              className='btn btn-delete'
            >
              🗑️ Delete
            </button>
          </div>
        </div>
      )}

      {/* EDIT MODE */}
      {mode === 'edit' && (
        <div className='edit-mode'>
          <input
            type='text'
            defaultValue={todo.text}
            onChange={handleInputChange}
            className='edit-input'
            placeholder='Edit todo...'
            autoFocus
          />
          <div className='actions'>
            <button
              onClick={handleSave}
              className='btn btn-save'
            >
              ✓ Save
            </button>
            <button
              onClick={handleCancel}
              className='btn btn-cancel'
            >
              ✗ Cancel
            </button>
          </div>
        </div>
      )}

      {/* DELETE CONFIRM MODE */}
      {mode === 'delete-confirm' && (
        <div className='delete-confirm-mode'>
          <p className='confirm-message'>
            Are you sure you want to delete "{todo.text}"?
          </p>
          <div className='actions'>
            <button
              onClick={handleConfirmDelete}
              className='btn btn-danger'
            >
              Yes, Delete
            </button>
            <button
              onClick={handleCancelDelete}
              className='btn btn-secondary'
            >
              No, Cancel
            </button>
          </div>
        </div>
      )}

      <div className='mode-indicator'>
        Current mode: <strong>{mode}</strong> (check console)
      </div>
    </div>
  );
}

// Test Component
function App() {
  const todos = [
    { id: 1, text: 'Learn React Events' },
    { id: 2, text: 'Master Conditional Rendering' },
    { id: 3, text: 'Build Real Projects' },
  ];

  return (
    <div className='app'>
      <h2>Interactive Todo Items</h2>
      <div className='todo-list'>
        {todos.map((todo) => (
          <TodoItem
            key={todo.id}
            todo={todo}
          />
        ))}
      </div>
    </div>
  );
}

CSS:

css
.app {
  max-width: 600px;
  margin: 20px auto;
  padding: 20px;
  color: #121212;
}

.todo-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.todo-item {
  padding: 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  background: white;
  transition: all 0.3s;
}

.todo-item.mode-edit {
  border-color: #1976d2;
  background: #e3f2fd;
}

.todo-item.mode-delete-confirm {
  border-color: #d32f2f;
  background: #ffebee;
}

.view-mode,
.edit-mode,
.delete-confirm-mode {
  display: flex;
  align-items: center;
  gap: 12px;
}

.todo-text {
  flex: 1;
  font-size: 16px;
}

.edit-input {
  flex: 1;
  padding: 8px 12px;
  font-size: 14px;
  border: 2px solid #1976d2;
  border-radius: 4px;
}

.confirm-message {
  flex: 1;
  margin: 0;
  font-size: 14px;
  color: #d32f2f;
}

.actions {
  display: flex;
  gap: 8px;
}

.btn {
  padding: 8px 16px;
  font-size: 14px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-edit {
  background: #1976d2;
  color: white;
}

.btn-edit:hover {
  background: #1565c0;
}

.btn-delete {
  background: #d32f2f;
  color: white;
}

.btn-delete:hover {
  background: #c62828;
}

.btn-save {
  background: #388e3c;
  color: white;
}

.btn-save:hover {
  background: #2e7d32;
}

.btn-cancel,
.btn-secondary {
  background: #757575;
  color: white;
}

.btn-cancel:hover,
.btn-secondary:hover {
  background: #616161;
}

.btn-danger {
  background: #d32f2f;
  color: white;
}

.btn-danger:hover {
  background: #c62828;
}

.mode-indicator {
  margin-top: 12px;
  padding: 8px;
  background: #f5f5f5;
  border-radius: 4px;
  font-size: 12px;
  color: #666;
  text-align: center;
}

📚 Key Learnings:

  • Mode-based conditional rendering
  • Multiple event handlers
  • Nested conditionals
  • Visual feedback for different states
  • Important: Without useState, mode changes don't persist visually!

⭐⭐⭐⭐ Exercise 4: Keyboard Shortcuts (60 phút)

🎯 Mục tiêu: Advanced event handling với keyboard
⏱️ Thời gian: 60 phút

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

Thiết kế một command palette (giống VS Code/Spotlight) với keyboard shortcuts. Các tính năng cần có:

  • Nhấn Cmd/Ctrl + K → Mở command palette
  • Nhấn Esc → Đóng command palette
  • Phím mũi tên → Di chuyển giữa các lựa chọn
  • Phím Enter → Chọn lựa chọn
  • Gõ để lọc danh sách lựa chọn

🤔 Các quyết định thiết kế (Design Decisions):

  1. Cấp độ gắn Event Listener:

    • Phương án A: Mức document (toàn cục)
    • Phương án B: Mức component (cục bộ)
    • Quyết định: ?
  2. Phát hiện tổ hợp phím:

    • Làm sao phân biệt Cmd + K với chỉ nhấn K?
    • Xử lý khác nhau giữa Ctrl (Windows) và Cmd (Mac) như thế nào?
  3. Quản lý Focus:

    • Có tự động focus vào ô input khi mở không?
    • Có chặn scroll của body khi palette đang mở không?

Mẫu ADR (Architecture Decision Record):

markdown
## Quyết định: Chiến lược xử lý sự kiện bàn phím

### Bối cảnh

Cần triển khai phím tắt toàn cục cho command palette.

### Quyết định

[Lựa chọn của bạn]

### Lý do

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

### Cách triển khai

[Phác thảo code]
💡 Solution
jsx
function CommandPalette() {
  let isOpen = false;
  let selectedIndex = 0;
  let searchQuery = '';

  const commands = [
    { id: 1, name: 'Create New File', shortcut: 'Cmd+N' },
    { id: 2, name: 'Open File', shortcut: 'Cmd+O' },
    { id: 3, name: 'Save File', shortcut: 'Cmd+S' },
    { id: 4, name: 'Search', shortcut: 'Cmd+F' },
    { id: 5, name: 'Settings', shortcut: 'Cmd+,' },
  ];

  // Filter commands based on search
  const filteredCommands = commands.filter((cmd) =>
    cmd.name.toLowerCase().includes(searchQuery.toLowerCase())
  );

  // Global keyboard handler
  const handleGlobalKeyDown = (event) => {
    // Cmd+K or Ctrl+K to toggle
    if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
      event.preventDefault();
      isOpen = !isOpen;
      console.log('Palette:', isOpen ? 'OPEN' : 'CLOSED');
      return;
    }

    // Only handle if palette is open
    if (!isOpen) return;

    // Esc to close
    if (event.key === 'Escape') {
      isOpen = false;
      console.log('Palette: CLOSED');
      return;
    }

    // Arrow keys navigation
    if (event.key === 'ArrowDown') {
      event.preventDefault();
      selectedIndex = (selectedIndex + 1) % filteredCommands.length;
      console.log('Selected:', filteredCommands[selectedIndex].name);
    }

    if (event.key === 'ArrowUp') {
      event.preventDefault();
      selectedIndex =
        (selectedIndex - 1 + filteredCommands.length) % filteredCommands.length;
      console.log('Selected:', filteredCommands[selectedIndex].name);
    }

    // Enter to execute
    if (event.key === 'Enter') {
      event.preventDefault();
      const selected = filteredCommands[selectedIndex];
      console.log('Executing:', selected.name);
      isOpen = false;
    }
  };

  const handleSearchChange = (event) => {
    searchQuery = event.target.value;
    selectedIndex = 0; // Reset selection
    console.log('Search:', searchQuery);
  };

  const handleCommandClick = (command) => {
    console.log('Clicked:', command.name);
    isOpen = false;
  };

  // Attach global listener (in real app: useEffect)
  if (typeof window !== 'undefined') {
    window.addEventListener('keydown', handleGlobalKeyDown);
  }

  return (
    <div className='command-palette-demo'>
      <div className='instructions'>
        <h3>Command Palette Demo</h3>
        <p>
          Press <kbd>Cmd/Ctrl + K</kbd> to open (check console)
        </p>
        <p>Arrow keys to navigate, Enter to select, Esc to close</p>
      </div>

      {/* Backdrop */}
      {isOpen && (
        <div
          className='backdrop'
          onClick={() => {
            isOpen = false;
            console.log('Backdrop clicked - CLOSED');
          }}
        />
      )}

      {/* Palette */}
      {isOpen && (
        <div className='palette'>
          <input
            type='text'
            className='search-input'
            placeholder='Type a command...'
            onChange={handleSearchChange}
            autoFocus
          />

          <div className='command-list'>
            {filteredCommands.length > 0 ? (
              filteredCommands.map((command, index) => (
                <div
                  key={command.id}
                  className={`command-item ${
                    index === selectedIndex ? 'selected' : ''
                  }`}
                  onClick={() => handleCommandClick(command)}
                >
                  <span className='command-name'>{command.name}</span>
                  <kbd className='command-shortcut'>{command.shortcut}</kbd>
                </div>
              ))
            ) : (
              <div className='no-results'>No commands found</div>
            )}
          </div>

          <div className='palette-footer'>
            <span>↑↓ Navigate</span>
            <span>↵ Select</span>
            <span>Esc Close</span>
          </div>
        </div>
      )}
    </div>
  );
}

CSS:

css
.command-palette-demo {
  min-height: 400px;
  position: relative;
}

.instructions {
  text-align: center;
  padding: 40px 20px;
}

.instructions kbd {
  padding: 4px 8px;
  background: #f5f5f5;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
}

.backdrop {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 1000;
}

.palette {
  position: fixed;
  top: 100px;
  left: 50%;
  transform: translateX(-50%);
  width: 90%;
  max-width: 600px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
  z-index: 1001;
  overflow: hidden;
}

.search-input {
  width: 100%;
  padding: 16px 20px;
  font-size: 16px;
  border: none;
  border-bottom: 1px solid #e0e0e0;
  outline: none;
}

.command-list {
  max-height: 400px;
  overflow-y: auto;
}

.command-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 20px;
  cursor: pointer;
  transition: background 0.2s;
}

.command-item:hover,
.command-item.selected {
  background: #f5f5f5;
}

.command-item.selected {
  background: #e3f2fd;
}

.command-name {
  font-size: 14px;
}

.command-shortcut {
  padding: 2px 6px;
  background: #f5f5f5;
  border: 1px solid #ccc;
  border-radius: 3px;
  font-size: 11px;
  font-family: monospace;
}

.no-results {
  padding: 40px 20px;
  text-align: center;
  color: #999;
}

.palette-footer {
  display: flex;
  gap: 16px;
  justify-content: center;
  padding: 12px;
  background: #f9f9f9;
  border-top: 1px solid #e0e0e0;
  font-size: 12px;
  color: #666;
}

📚 Áp dụng các kỹ thuật trong ví dụ CommandPalette:

  • Global event listener Sử dụng window.addEventListener('keydown', handleGlobalKeyDown) để lắng nghe phím tắt ở toàn bộ ứng dụng, dù focus đang ở đâu.

  • Keyboard shortcuts detection Kiểm tra event.metaKey || event.ctrlKey kết hợp với event.key === 'k' để mở/đóng Command Palette bằng Cmd/Ctrl + K.

  • Focus management Khi palette mở, input search được autoFocus để người dùng có thể gõ ngay mà không cần click.

  • Arrow key navigation Dùng ArrowUp / ArrowDown để thay đổi selectedIndex, cho phép di chuyển giữa các command trong danh sách.

  • Conditional backdrop rendering Backdrop chỉ được render khi isOpen === true, click vào backdrop sẽ đóng palette.

  • Search filtering Lọc danh sách commands theo searchQuery, cập nhật realtime khi người dùng nhập vào ô search.

  • Conditional rendering UI Palette, backdrop và danh sách command chỉ xuất hiện khi isOpentrue.

  • Command execution logic Nhấn Enter hoặc click vào command sẽ thực thi hành động tương ứng và đóng palette.

  • Lưu ý quan trọngisOpen, selectedIndex, searchQuery đang là biến thường → không persist giữa các lần render. Trong app thực tế, các giá trị này cần được quản lý bằng useState và global listener cần gắn/gỡ bằng useEffect.


⭐⭐⭐⭐⭐ Exercise 5: Multi-State UI Component (90 phút)

🎯 Mục tiêu: Production-ready component với nhiều states
⏱️ Thời gian: 90 phút

📋 Feature Specification:

Tạo File Upload component với các states:

  • Idle: Chưa có file
  • Dragging: Đang drag file vào
  • Uploading: Đang upload (fake với setTimeout)
  • Success: Upload thành công
  • Error: Upload thất bại

✅ Production Checklist:

Features:

  • [ ] Drag & drop file upload
  • [ ] Click to browse files
  • [ ] File type validation (images only)
  • [ ] File size validation (< 5MB)
  • [ ] Upload progress simulation
  • [ ] Success/Error messages
  • [ ] Retry on error

Accessibility:

  • [ ] Keyboard accessible
  • [ ] ARIA labels
  • [ ] Focus management

💡 Solution
jsx
function FileUploader() {
  // States: 'idle' | 'dragging' | 'uploading' | 'success' | 'error'
  let state = 'idle';
  let file = null;
  let errorMessage = '';
  let progress = 0;

  // Validation
  const validateFile = (file) => {
    const maxSize = 5 * 1024 * 1024; // 5MB
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];

    if (!allowedTypes.includes(file.type)) {
      return { valid: false, error: 'Only images (JPG, PNG, GIF) allowed' };
    }

    if (file.size > maxSize) {
      return { valid: false, error: 'File size must be less than 5MB' };
    }

    return { valid: true };
  };

  // Simulate upload :  Giả lập upload (CHỈ DÙNG DEMO – KHÔNG DÙNG TRONG PRODUCTION)
  const simulateUpload = (file) => {
    state = 'uploading'; // trạng thái upload (idle | uploading | success | error)
    progress = 0; // phần trăm tiến trình (0–100)

    // ❌ Progress giả bằng setInterval – KHÔNG dùng thực tế
    // ✅ Production – nhiều lớp xử lý:
    // 1️⃣ Upload progress (client → server)
    //    - XMLHttpRequest.upload.onprogress
    //    - Axios onUploadProgress
    //
    // 2️⃣ Server-side progress (NÂNG CAO)
    //    - Upload xong ≠ xử lý xong
    //    - Server xử lý lâu: virus scan, resize ảnh, unzip, import data...
    //    - Client upload đạt 100% nhưng UI vẫn đang "processing"
    //
    // Luồng chuẩn:
    // - Client upload file
    // - Server trả về jobId
    // - Server xử lý async
    // - Client dùng polling / WebSocket / SSE để lấy progress xử lý
    //
    // Ví dụ UI:
    // - Uploading: 0% → 70%
    // - Processing: 70% → 100%

    const interval = setInterval(() => {
      progress += 10; // tăng progress giả
      console.log('Progress:', progress + '%');

      if (progress >= 100) {
        clearInterval(interval);

        // ❌ Random success/fail – chỉ để minh họa
        // ✅ Production:
        // - Dựa vào HTTP status (200 / 4xx / 5xx)
        // - Response trả về từ server
        // - Retry / backoff khi lỗi mạng

        if (Math.random() > 0.3) {
          state = 'success'; // upload thành công
          console.log('Upload SUCCESS');
        } else {
          state = 'error'; // upload thất bại
          errorMessage = 'Upload failed. Please try again.';
          console.log('Upload ERROR');
        }
      }
    }, 200);
  };

  const handleDragOver = (event) => {
    event.preventDefault();
    state = 'dragging';
  };

  const handleDragLeave = (event) => {
    event.preventDefault();
    state = 'idle';
  };

  const handleDrop = (event) => {
    event.preventDefault();
    state = 'idle';

    const droppedFile = event.dataTransfer.files[0];
    if (droppedFile) {
      handleFileSelect(droppedFile);
    }
  };

  const handleFileInputChange = (event) => {
    const selectedFile = event.target.files[0];
    if (selectedFile) {
      handleFileSelect(selectedFile);
    }
  };

  const handleFileSelect = (selectedFile) => {
    const validation = validateFile(selectedFile);

    if (!validation.valid) {
      state = 'error';
      errorMessage = validation.error;
      console.log('Validation Error:', validation.error);
      return;
    }

    file = selectedFile;
    simulateUpload(file);
  };

  const handleRetry = () => {
    if (file) {
      simulateUpload(file);
    }
  };

  const handleReset = () => {
    state = 'idle';
    file = null;
    errorMessage = '';
    progress = 0;
  };

  // Render different UIs based on state
  return (
    <div className='file-uploader'>
      <h2>File Upload Component</h2>

      {/* IDLE STATE */}
      {state === 'idle' && (
        <div
          className='upload-zone'
          onDragOver={handleDragOver}
          onDragLeave={handleDragLeave}
          onDrop={handleDrop}
        >
          <div className='upload-icon'>📁</div>
          <p>Drag & drop an image here</p>
          <p className='text-secondary'>or</p>
          <label className='browse-btn'>
            Browse Files
            <input
              type='file'
              accept='image/*'
              onChange={handleFileInputChange}
              style={{ display: 'none' }}
            />
          </label>
          <p className='file-requirements'>JPG, PNG or GIF • Max 5MB</p>
        </div>
      )}

      {/* DRAGGING STATE */}
      {state === 'dragging' && (
        <div
          className='upload-zone dragging'
          onDragOver={handleDragOver}
          onDragLeave={handleDragLeave}
          onDrop={handleDrop}
        >
          <div className='upload-icon'>⬇️</div>
          <p className='highlight'>Drop your file here!</p>
        </div>
      )}

      {/* UPLOADING STATE */}
      {state === 'uploading' && (
        <div className='upload-zone uploading'>
          <div className='spinner'>⏳</div>
          <p>Uploading {file?.name}...</p>
          <div className='progress-bar'>
            <div
              className='progress-fill'
              style={{ width: `${progress}%` }}
            />
          </div>
          <p className='progress-text'>{progress}%</p>
        </div>
      )}

      {/* SUCCESS STATE */}
      {state === 'success' && (
        <div className='upload-zone success'>
          <div className='success-icon'>✅</div>
          <p className='success-text'>Upload successful!</p>
          <p className='file-name'>{file?.name}</p>
          <button
            onClick={handleReset}
            className='btn-secondary'
          >
            Upload Another
          </button>
        </div>
      )}

      {/* ERROR STATE */}
      {state === 'error' && (
        <div className='upload-zone error'>
          <div className='error-icon'>❌</div>
          <p className='error-text'>Upload failed</p>
          <p className='error-message'>{errorMessage}</p>
          <div className='error-actions'>
            <button
              onClick={handleRetry}
              className='btn-primary'
            >
              Retry
            </button>
            <button
              onClick={handleReset}
              className='btn-secondary'
            >
              Cancel
            </button>
          </div>
        </div>
      )}

      <div className='state-indicator'>
        Current state: <strong>{state}</strong>
      </div>
    </div>
  );
}
css
.file-uploader {
  max-width: 500px;
  margin: 20px auto;
  padding: 20px;
}

.upload-zone {
  border: 3px dashed #e0e0e0;
  border-radius: 8px;
  padding: 60px 40px;
  text-align: center;
  transition: all 0.3s;
  background: #fafafa;
}

.upload-zone.dragging {
  border-color: #1976d2;
  background: #e3f2fd;
  transform: scale(1.02);
}

.upload-zone.uploading {
  border-color: #1976d2;
  background: #e3f2fd;
}

.upload-zone.success {
  border-color: #388e3c;
  background: #e8f5e9;
}

.upload-zone.error {
  border-color: #d32f2f;
  background: #ffebee;
}

.upload-icon {
  font-size: 64px;
  margin-bottom: 16px;
}

.upload-zone p {
  margin: 8px 0;
  color: #333;
}

.text-secondary {
  color: #999;
  font-size: 14px;
}

.browse-btn {
  display: inline-block;
  padding: 10px 24px;
  margin: 16px 0;
  background: #1976d2;
  color: white;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 500;
  transition: background 0.2s;
}

.browse-btn:hover {
  background: #1565c0;
}

.file-requirements {
  font-size: 12px;
  color: #999;
  margin-top: 16px;
}

.highlight {
  font-size: 18px;
  font-weight: 600;
  color: #1976d2;
}

/* Uploading State */
.spinner {
  font-size: 48px;
  margin-bottom: 16px;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.progress-bar {
  width: 100%;
  height: 8px;
  background: #e0e0e0;
  border-radius: 4px;
  overflow: hidden;
  margin: 16px 0;
}

.progress-fill {
  height: 100%;
  background: #1976d2;
  transition: width 0.3s ease;
}

.progress-text {
  font-size: 14px;
  font-weight: 600;
  color: #1976d2;
}

/* Success State */
.success-icon {
  font-size: 64px;
  margin-bottom: 16px;
}

.success-text {
  font-size: 20px;
  font-weight: 600;
  color: #388e3c;
}

.file-name {
  font-size: 14px;
  color: #666;
  margin: 8px 0 24px 0;
}

/* Error State */
.error-icon {
  font-size: 64px;
  margin-bottom: 16px;
}

.error-text {
  font-size: 20px;
  font-weight: 600;
  color: #d32f2f;
}

.error-message {
  font-size: 14px;
  color: #d32f2f;
  margin: 8px 0 24px 0;
}

.error-actions {
  display: flex;
  gap: 12px;
  justify-content: center;
}

/* Buttons */
.btn-primary,
.btn-secondary {
  padding: 10px 24px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-primary {
  background: #1976d2;
  color: white;
}

.btn-primary:hover {
  background: #1565c0;
}

.btn-secondary {
  background: #e0e0e0;
  color: #333;
}

.btn-secondary:hover {
  background: #d0d0d0;
}

/* State Indicator */
.state-indicator {
  margin-top: 20px;
  padding: 12px;
  background: #f5f5f5;
  border-radius: 4px;
  text-align: center;
  font-size: 13px;
  color: #666;
}

.state-indicator strong {
  color: #1976d2;
  text-transform: uppercase;
}

📚 Best Practices trong Production đã áp dụng:

  1. State Machine Pattern (Mô hình trạng thái):

    • Luồng chuyển trạng thái rõ ràng
    • Mỗi trạng thái tương ứng với một UI cụ thể
    • Tránh trạng thái mơ hồ, khó kiểm soát
  2. Validation (Kiểm tra dữ liệu):

    • Validate file phía client
    • Thông báo lỗi rõ ràng, dễ hiểu
    • Kiểm tra loại file và kích thước file
  3. User Feedback (Phản hồi cho người dùng):

    • Hiển thị loading spinner
    • Thanh tiến trình (progress bar)
    • Trạng thái thành công / thất bại
    • Cơ chế retry khi lỗi
  4. Accessibility (Khả năng truy cập):

    • Sử dụng semantic HTML
    • Có thể thao tác hoàn toàn bằng bàn phím (file input)
    • Label rõ ràng cho các trường nhập
    • Phản hồi trực quan cho người dùng
  5. Error Handling (Xử lý lỗi):

    • Xử lý lỗi một cách an toàn, không làm sập UI
    • Cho phép thử lại khi gặp lỗi
    • Thông báo thân thiện với người dùng

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

So sánh các pattern xử lý event

PatternVí dụ codeƯu điểm ✅Nhược điểm ❌Khi nên dùng
Function ReferenceonClick={handleClick}• Gọn gàng
• Tối ưu hiệu năng
• Dễ tái sử dụng
• Không truyền được tham số
• Ít linh hoạt
Handler đơn giản, không cần tham số
Inline Arrow FunctiononClick={() => handleClick(id)}• Truyền được tham số
• Linh hoạt
• Tạo function mới mỗi lần render
• Khó test hơn
Khi cần truyền tham số
Higher-Order FunctiononClick={handleClick(id)}• Code sạch
• Không dùng arrow inline
• Tái sử dụng tốt
• Khó hiểu hơn
• Có độ dốc học tập
Khi có nhiều handler tương tự nhau
Bind MethodonClick={handleClick.bind(this, id)}• Truyền được tham số• Pattern cũ
• Ảnh hưởng hiệu năng
• Dễ gây nhầm lẫn
❌ Nên tránh, ưu tiên arrow function

Bảng so sánh các pattern Conditional Rendering

PatternCú phápƯu điểm ✅Nhược điểm ❌Phù hợp khi
If / Elseif (condition) return <A />; return <B />;• Dễ đọc
• Viết được nhiều câu lệnh
• Hỗ trợ early return
• Dài dòng
• Không dùng trực tiếp trong JSX
Logic phức tạp, cần return sớm
Ternary{condition ? <A /> : <B />}• Ngắn gọn
• Dùng inline trong JSX
• Phù hợp JSX
• Khó đọc khi lồng nhiều tầng
• Chỉ dùng cho biểu thức
Điều kiện true / false đơn giản
Logical &&{condition && <Component />}• Rất gọn
• Pattern phổ biến
• Có thể render ra 0
• Chỉ phù hợp để ẩn/hiện
Hiển thị component theo điều kiện
Switchswitch (value) { case 'a': return <A /> }• Rõ ràng với nhiều nhánh
• Dễ mở rộng
• Dài dòng
• Bắt buộc return
Khi có nhiều lựa chọn rời rạc
Object Mapconst views = { a: <A /> }; return views[key];• Dễ mở rộng
• Theo hướng dữ liệu
• Cần setup trước
• Khó hiểu với người mới
Chọn component động theo key

Decision Tree: Conditional Rendering

Cần hiển thị component có điều kiện?

├─ Có 2 lựa chọn (A hoặc B)?
│   ├─ Biểu thức đơn giản?
│   │   └─ ✅ Ternary: {condition ? <A /> : <B />}
│   │
│   └─ Logic phức tạp?
│       └─ ✅ If / Else: if (condition) return <A />

├─ Chỉ cần ẩn / hiện (có hoặc không)?
│   ├─ Điều kiện là boolean?
│   │   └─ ✅ Toán tử &&: {isVisible && <Component />}
│   │
│   └─ Điều kiện là số?
│       └─ ✅ So sánh rõ ràng: {count > 0 && <Component />}

├─ Nhiều trạng thái (3+)?
│   ├─ Các trạng thái liên quan (giống enum)?
│   │   └─ ✅ Switch hoặc Object Map
│   │
│   └─ Điều kiện phức tạp?
│       └─ ✅ Chuỗi If / Else với early return

└─ Điều kiện lồng nhau?
    ├─ Có thể làm phẳng?
    │   └─ ✅ Tách ra function hoặc component riêng

    └─ Không thể làm phẳng?
        └─ ✅ Dùng nhiều if hoặc guard clauses

Decision Tree: Xử lý Event

Cần xử lý event?

├─ Handler đơn giản, không cần tham số?
│   └─ ✅ Tham chiếu hàm: onClick={handleClick}

├─ Cần truyền tham số?
│   ├─ Chỉ một handler?
│   │   └─ ✅ Arrow function inline: onClick={() => handleClick(id)}
│   │
│   └─ Nhiều handler tương tự nhau?
│       └─ ✅ Higher-order function:
│           const handler = (id) => () => { ... }

├─ Cần dùng object event?
│   ├─ Chỉ cần event?
│   │   └─ ✅ Tham chiếu hàm: onClick={handleClick}
│   │       // function handleClick(event) { ... }
│   │
│   └─ Cần cả event + tham số?
│       └─ ✅ Arrow function có event:
│           onClick={(e) => handleClick(e, id)}

└─ Cần chặn hành vi mặc định?
    └─ ✅ Gọi event.preventDefault() trong handler
        // function handleSubmit(event) {
        //   event.preventDefault();
        //   ...
        // }

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

Bug 1: Function Called Immediately (Hàm được gọi ngay lập tức) ❌

jsx
// 🐛 CODE BỊ LỖI:
function Counter() {
  let count = 0;

  const increment = () => {
    count++;
    console.log('Count:', count);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment()}>Increment</button>
    </div>
  );
}

❓ Câu hỏi:

  1. Lỗi gì xảy ra?
  2. Tại sao function chạy ngay?
  3. Làm sao fix?
💡 Giải đáp

1. Lỗi:

  • Function increment() chạy NGAY KHI RENDER
  • Console.log xuất hiện ngay, không đợi click
  • Button không hoạt động khi click

2. Tại sao:

jsx
onClick={increment()}  // ❌ Calls function immediately
//         ^^ parentheses execute the function NOW

// What React sees:
onClick={undefined}  // Result of increment() call

3. Fix:

jsx
// ✅ Solution 1: Remove parentheses
<button onClick={increment}>Increment</button>

// ✅ Solution 2: Wrap in arrow function
<button onClick={() => increment()}>Increment</button>

// ✅ Solution 3: If need to pass args
<button onClick={() => increment(5)}>Increment by 5</button>

🎯 Remember:

  • onClick={func} → pass reference
  • onClick={func()} → execute now ❌
  • onClick={() => func()} → execute on click ✅

Bug 2: Missing preventDefault ❌

jsx
// 🐛 CODE BỊ LỖI:
function SearchForm() {
  const handleSubmit = () => {
    console.log('Searching...');
    // Do search logic
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type='text'
        placeholder='Search...'
      />
      <button type='submit'>Search</button>
    </form>
  );
}

❓ Câu hỏi:

  1. Chuyện gì xảy ra khi submit form?
  2. Tại sao console.log không thấy?
  3. Làm sao fix?
💡 Giải đáp

1. Chuyện gì xảy ra:

  • Page RELOAD ngay lập tức
  • Form submit theo default browser behavior
  • Console.log bị clear do reload

2. Tại sao không thấy log:

  • Browser default: form submit → page reload
  • Reload happens trước khi console.log visible
  • Không có time để thấy log

3. Fix:

jsx
// ✅ CORRECT: Prevent default behavior
function SearchForm() {
  const handleSubmit = (event) => {
    event.preventDefault(); // CRITICAL!
    console.log('Searching...');
    // Now can do search logic
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type='text'
        placeholder='Search...'
      />
      <button type='submit'>Search</button>
    </form>
  );
}

🎯 Always preventDefault for:

  • Form submissions
  • Link clicks (if custom handling)
  • Context menus
  • Any default browser behavior you want to override

Bug 3: Logical && Renders 0 ❌

jsx
// 🐛 CODE BỊ LỖI:
function ShoppingCart({ items }) {
  const itemCount = items.length;

  return (
    <div>
      <h2>Shopping Cart</h2>

      {itemCount && <p>You have {itemCount} items</p>}

      {!itemCount && <p>Your cart is empty</p>}
    </div>
  );
}

// Test
<ShoppingCart items={[]} />;

❓ Câu hỏi:

  1. Output là gì khi items = []?
  2. Tại sao xuất hiện số "0" trên screen?
  3. Làm sao fix?
💡 Giải đáp

1. Output:

html
<div>
  <h2>Shopping Cart</h2>
  0
  <!-- ❌ Oops! Number 0 is rendered -->
  <p>Your cart is empty</p>
</div>

2. Tại sao:

jsx
{
  itemCount && <Component />;
}
// When itemCount = 0:

{
  0 && <Component />;
}
// Evaluates to: 0

// React renders: 0

JavaScript falsy values:

  • false → not rendered
  • null → not rendered
  • undefined → not rendered
  • "" → not rendered
  • 0 → RENDERED as "0" ⚠️
  • NaN → rendered as "NaN"

3. Fix:

jsx
// ❌ BAD: Can render 0
{
  itemCount && <p>You have {itemCount} items</p>;
}

// ✅ GOOD: Explicit comparison
{
  itemCount > 0 && <p>You have {itemCount} items</p>;
}

// ✅ GOOD: Boolean conversion
{
  Boolean(itemCount) && <p>You have {itemCount} items</p>;
}

// ✅ GOOD: Ternary for both cases
{
  itemCount > 0 ? <p>You have {itemCount} items</p> : <p>Your cart is empty</p>;
}

🎯 Best Practice: Always use explicit comparisons with &&:

  • count > 0 && ...
  • items.length > 0 && ...
  • value != null && ...

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

Knowledge Check

  • [ ] Tôi hiểu event handling trong React (declarative)
  • [ ] Tôi biết SyntheticEvent là gì và tại sao React dùng nó
  • [ ] Tôi phân biệt được onClick={func} vs onClick={func()}
  • [ ] Tôi biết khi nào dùng event.preventDefault()
  • [ ] Tôi biết khi nào dùng event.stopPropagation()
  • [ ] Tôi thành thạo if/else, ternary, && cho conditional rendering
  • [ ] Tôi biết gotcha của && với falsy values (số 0)
  • [ ] Tôi biết cách handle keyboard events
  • [ ] Tôi biết cách pass arguments vào event handlers
  • [ ] Tôi phân biệt được khi nào dùng pattern nào

Code Review Checklist

Khi viết event handlers:

  • [ ] Naming: handle[Event] pattern (handleClick, handleSubmit)
  • [ ] Prevent default: Dùng preventDefault() cho forms, links
  • [ ] Function reference: Pass reference, không call func()
  • [ ] Arguments: Dùng arrow wrapper nếu cần pass args
  • [ ] Event object: Access qua parameter nếu cần
  • [ ] Cleanup: Consider stopPropagation() nếu cần

Khi viết conditional rendering:

  • [ ] Simple conditions: Dùng ternary hoặc &&
  • [ ] Complex logic: Extract to if/else or separate function
  • [ ] Avoid nesting: Nested ternary là anti-pattern
  • [ ] Falsy values: Explicit comparison với &&
  • [ ] Readability: Code clear > clever

🏠 BÀI TẬP VỀ NHÀ

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

1. Toggle Switch Component

Tạo toggle switch (như dark mode toggle) với:

  • Click to toggle state (console.log state)
  • Visual feedback (on/off states)
  • Label thay đổi: "Dark Mode: On" / "Dark Mode: Off"
  • Conditional styling (background color)
jsx
// Expected behavior:
// Click → toggle → log "Dark Mode: on"
// Click → toggle → log "Dark Mode: off"
💡 Solution - Bài tập về nhà 1: Toggle Switch Component
jsx
// Bài tập về nhà - Ngày 5
// Bài 1: Toggle Switch Component (Dark Mode Toggle demo)
// - Click để chuyển đổi trạng thái
// - Hiển thị trạng thái "Dark Mode: On / Off"
// - Thay đổi giao diện (màu nền, màu chữ) theo trạng thái
// - Log trạng thái ra console mỗi lần toggle

// =============================================
// ToggleSwitch Component
// =============================================
function ToggleSwitch() {
  // Biến trạng thái (sẽ học useState ở ngày sau - hiện tại dùng biến thường để demo)
  // Trong thực tế: dùng useState để UI cập nhật lại khi thay đổi
  let isDarkMode = false;

  const handleToggle = () => {
    // Toggle giá trị
    isDarkMode = !isDarkMode;

    // Log trạng thái
    console.log(`Dark Mode: ${isDarkMode ? 'ON' : 'OFF'}`);

    // Cập nhật giao diện (trong thực tế sẽ re-render nhờ state)
    // Ở đây ta chỉ log và giả lập thay đổi style
    document.body.style.backgroundColor = isDarkMode ? '#111827' : '#f3f4f6';
    document.body.style.color = isDarkMode ? '#f3f4f6' : '#111827';
  };

  return (
    <div
      style={{
        minHeight: '100vh',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        padding: '40px',
        fontFamily: 'system-ui, sans-serif',
        transition: 'all 0.4s ease',
        backgroundColor: isDarkMode ? '#111827' : '#f3f4f6',
        color: isDarkMode ? '#f3f4f6' : '#111827',
      }}
    >
      <h1 style={{ marginBottom: '32px', fontSize: '2.5rem' }}>
        Dark Mode Toggle
      </h1>

      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: '24px',
          background: isDarkMode ? '#374151' : 'white',
          padding: '16px 32px',
          borderRadius: '999px',
          boxShadow: '0 10px 25px rgba(0,0,0,0.1)',
        }}
      >
        <span
          style={{
            fontSize: '1.3rem',
            fontWeight: '600',
            minWidth: '180px',
            textAlign: 'center',
          }}
        >
          Dark Mode: {isDarkMode ? 'ON' : 'OFF'}
        </span>

        {/* Toggle Switch UI */}
        <label
          style={{
            position: 'relative',
            display: 'inline-block',
            width: '80px',
            height: '44px',
            cursor: 'pointer',
          }}
        >
          <input
            type='checkbox'
            checked={isDarkMode}
            onChange={handleToggle}
            style={{
              opacity: 0,
              width: 0,
              height: 0,
            }}
          />
          <span
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              right: 0,
              bottom: 0,
              backgroundColor: isDarkMode ? '#10b981' : '#ccc',
              borderRadius: '44px',
              transition: 'all 0.3s ease',
            }}
          />
          <span
            style={{
              position: 'absolute',
              content: '""',
              height: '36px',
              width: '36px',
              left: '4px',
              bottom: '4px',
              backgroundColor: 'white',
              borderRadius: '50%',
              transition: 'all 0.3s ease',
              transform: isDarkMode ? 'translateX(36px)' : 'translateX(0)',
              boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
            }}
          />
        </label>
      </div>

      <p style={{ marginTop: '32px', fontSize: '1.1rem', opacity: 0.8 }}>
        Click switch hoặc nhấn phím Space khi focus để toggle
      </p>

      <div
        style={{
          marginTop: '40px',
          padding: '20px',
          background: isDarkMode ? '#1f2937' : '#ffffff',
          borderRadius: '12px',
          boxShadow: '0 4px 15px rgba(0,0,0,0.08)',
          maxWidth: '500px',
          textAlign: 'center',
        }}
      >
        <p style={{ margin: '0 0 16px 0' }}>
          Đây là nội dung demo. Khi bật Dark Mode:
        </p>
        <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
          <li>✔️ Nền tối, chữ sáng</li>
          <li>✔️ Switch chuyển sang màu xanh</li>
          <li>✔️ Console log trạng thái</li>
        </ul>
      </div>

      {/* Hiển thị trạng thái hiện tại (giúp debug) */}
      <div
        style={{
          marginTop: '40px',
          padding: '12px 24px',
          background: isDarkMode ? '#374151' : '#f3f4f6',
          borderRadius: '999px',
          fontSize: '0.95rem',
          color: isDarkMode ? '#d1d5db' : '#4b5563',
        }}
      >
        Trạng thái hiện tại:{' '}
        <strong>{isDarkMode ? 'Dark Mode ON' : 'Dark Mode OFF'}</strong>
      </div>
    </div>
  );
}

/* 
  Một số cải tiến có thể thêm sau khi học useState (Ngày 11):
  - Thực sự thay đổi theme toàn app (dùng context hoặc CSS variables)
  - Lưu trạng thái vào localStorage
  - Hỗ trợ phím tắt (T) để toggle
  - Animation mượt hơn cho switch
  - Hiệu ứng chuyển theme mượt (transition trên body)
*/

Ghi chú quan trọng:

  • Hiện tại chưa dùng state → giao diện không thực sự cập nhật khi click (chỉ log console và thay đổi style thủ công).
    Sau khi học useState (Ngày 11), bạn có thể làm lại để UI thực sự thay đổi.

  • Điểm nổi bật:

    • Switch UI đẹp, hiện đại (giống toggle iOS)
    • Hover + transition mượt
    • Log trạng thái rõ ràng
    • Responsive và dễ nhìn trên mọi thiết bị
    • Accessibility cơ bản (có thể focus bằng Tab và nhấn Space/Enter để toggle)
  • Edge cases đã xử lý:

    • Ban đầu là OFF
    • Toggle nhiều lần → trạng thái đúng
    • Console log mỗi lần thay đổi
    • Giao diện thay đổi theo trạng thái (màu nền, chữ, switch)

2. Character Counter Input

Tạo textarea với character counter:

  • onChange để count characters
  • Display: "50/200 characters"
  • Warning nếu > 180: "Approaching limit!"
  • Error nếu > 200: "Limit exceeded!"
  • Conditional text color (green → yellow → red)
💡 Solution - Bài tập về nhà 2: Character Counter Input
jsx
// Bài tập về nhà - Ngày 5
// Bài 2: Character Counter Input (Textarea với bộ đếm ký tự)
// - Hiển thị số ký tự hiện tại / giới hạn
// - Cảnh báo khi gần vượt giới hạn (màu vàng)
// - Lỗi khi vượt quá giới hạn (màu đỏ, disable nút submit)
// - Cập nhật realtime khi gõ

// =============================================
// CharacterCounter Component
// =============================================
function CharacterCounter() {
  // Biến tạm (sẽ thay bằng useState ở ngày sau)
  // Hiện tại chỉ demo logic - UI không cập nhật realtime
  let text = '';
  const maxLength = 200;

  const handleTextChange = (event) => {
    text = event.target.value;

    const currentLength = text.length;
    console.log(`Ký tự: ${currentLength}/${maxLength}`);

    // Validation logic (sẽ dùng để điều khiển style)
    const isWarning = currentLength > 180 && currentLength <= maxLength;
    const isError = currentLength > maxLength;

    // Trong thực tế: cập nhật state → re-render UI
    console.log({
      length: currentLength,
      isWarning,
      isError,
      remaining: maxLength - currentLength,
    });
  };

  const getCounterStyle = () => {
    const length = text.length;

    if (length > 180 && length <= maxLength) {
      return { color: '#f59e0b' }; // vàng - cảnh báo
    }
    if (length > maxLength) {
      return { color: '#ef4444', fontWeight: 'bold' }; // đỏ - lỗi
    }
    return { color: '#6b7280' }; // xám bình thường
  };

  const isValid = text.length <= maxLength;

  return (
    <div
      style={{
        maxWidth: '600px',
        margin: '40px auto',
        padding: '32px',
        background: 'white',
        borderRadius: '12px',
        boxShadow: '0 10px 25px rgba(0,0,0,0.08)',
        fontFamily: 'system-ui, sans-serif',
      }}
    >
      <h2 style={{ marginBottom: '24px', color: '#1f2937' }}>
        Viết bình luận / status
      </h2>

      <div style={{ position: 'relative' }}>
        <textarea
          value={text} // sẽ bind với state sau
          onChange={handleTextChange}
          placeholder='Nhập nội dung của bạn ở đây... (tối đa 200 ký tự)'
          maxLength={maxLength}
          style={{
            width: '100%',
            minHeight: '140px',
            padding: '16px',
            fontSize: '16px',
            border: `2px solid ${
              text.length > maxLength
                ? '#ef4444'
                : text.length > 180
                ? '#f59e0b'
                : '#d1d5db'
            }`,
            borderRadius: '8px',
            resize: 'vertical',
            outline: 'none',
            transition: 'border-color 0.2s',
          }}
        />

        {/* Bộ đếm ký tự */}
        <div
          style={{
            position: 'absolute',
            bottom: '12px',
            right: '16px',
            fontSize: '14px',
            fontWeight: '500',
            ...getCounterStyle(),
          }}
        >
          {text.length} / {maxLength}
          {text.length > 180 &&
            text.length <= maxLength &&
            ' • Sắp hết giới hạn!'}
          {text.length > maxLength && ' • Vượt quá giới hạn!'}
        </div>
      </div>

      {/* Thông báo trạng thái */}
      <div style={{ marginTop: '16px', minHeight: '24px' }}>
        {text.length > 180 && text.length <= maxLength && (
          <span style={{ color: '#f59e0b', fontSize: '14px' }}>
            ⚠️ Bạn sắp đạt giới hạn ký tự
          </span>
        )}

        {text.length > maxLength && (
          <span style={{ color: '#ef4444', fontSize: '14px' }}>
            ✗ Nội dung vượt quá {maxLength} ký tự
          </span>
        )}
      </div>

      {/* Nút submit (demo) */}
      <div style={{ marginTop: '24px', textAlign: 'right' }}>
        <button
          disabled={!isValid || text.trim() === ''}
          style={{
            padding: '12px 32px',
            fontSize: '16px',
            fontWeight: '600',
            color: 'white',
            backgroundColor:
              isValid && text.trim() !== '' ? '#3b82f6' : '#9ca3af',
            border: 'none',
            borderRadius: '8px',
            cursor: isValid && text.trim() !== '' ? 'pointer' : 'not-allowed',
            transition: 'background-color 0.2s',
          }}
          onClick={() => {
            if (isValid && text.trim() !== '') {
              console.log('Gửi nội dung:', text);
              alert('Đã gửi thành công! (Demo)');
              text = ''; // reset (sẽ dùng state sau)
            }
          }}
        >
          Gửi
        </button>
      </div>

      {/* Hiển thị trạng thái debug */}
      <div
        style={{
          marginTop: '32px',
          padding: '12px',
          background: '#f3f4f6',
          borderRadius: '8px',
          fontSize: '14px',
          color: '#4b5563',
        }}
      >
        Trạng thái hiện tại:
        <strong style={{ color: '#3b82f6' }}>
          {text.length === 0
            ? 'Chưa nhập'
            : text.length <= 180
            ? 'Bình thường'
            : text.length <= 200
            ? 'Cảnh báo'
            : 'Lỗi'}
        </strong>
        {' • '}Ký tự: {text.length}/{maxLength}
      </div>
    </div>
  );
}

Các tính năng đã triển khai:

  • Realtime đếm ký tự (hiển thị X/200)
  • Màu sắc cảnh báo theo ngưỡng:
    • Xanh/xám: bình thường (0–180)
    • Vàng: cảnh báo (181–200)
    • Đỏ: lỗi (> 200)
  • Border input thay đổi màu theo trạng thái
  • Nút Gửi bị disable khi:
    • Vượt quá giới hạn
    • Không có nội dung (rỗng hoặc chỉ khoảng trắng)
  • Thông báo trạng thái rõ ràng
  • Debug panel nhỏ để xem trạng thái hiện tại

Edge cases đã xử lý:

  • Không nhập gì → nút Gửi disable
  • Nhập đúng 180 ký tự → cảnh báo vàng
  • Vượt 200 ký tự → lỗi đỏ, nút disable
  • Xóa về 0 → trở về trạng thái bình thường
  • Nhập khoảng trắng → vẫn tính là ký tự

Hạn chế hiện tại (vì chưa học state):

  • Gõ chữ → UI không cập nhật realtime (vẫn giữ giá trị ban đầu)
  • Chỉ log và thay đổi style thủ công

→ Sau khi học useState (ngày 11), bạn chỉ cần thay let text = '' bằng const [text, setText] = useState('') và thêm value={text} + onChange={(e) => setText(e.target.value)} là component sẽ hoạt động hoàn hảo.


Nâng cao (60 phút)

3. Tab Navigation Component

Tạo tab component (giống browser tabs):

  • 3-4 tabs với different content
  • Click tab → show corresponding content
  • Active tab có styling khác
  • Keyboard navigation: Arrow keys để switch tabs
  • Conditional rendering cho content

Example structure:

jsx
const tabs = [
  { id: 1, label: 'Profile', content: 'Profile content...' },
  { id: 2, label: 'Settings', content: 'Settings content...' },
  { id: 3, label: 'Notifications', content: 'Notifications...' },
];
💡 Solution - Bài 3 Nâng cao: Tab Navigation Component
jsx
// Bài tập về nhà Nâng cao - Ngày 5
// Bài 3: Tab Navigation Component
// - 4 tab với nội dung khác nhau
// - Click tab → chuyển nội dung tương ứng
// - Tab đang active có style nổi bật
// - Hỗ trợ điều hướng bằng phím mũi tên (Arrow Left/Right)
// - Responsive: tab cuộn ngang trên mobile nếu quá nhiều

// =============================================
// TabNavigation Component
// =============================================
function TabNavigation() {
  // Trạng thái tab đang active (sẽ học useState ngày sau)
  // Hiện tại dùng biến thường để demo logic
  let activeTabId = 1; // mặc định tab đầu tiên

  // Dữ liệu các tab
  const tabs = [
    {
      id: 1,
      label: 'Profile',
      icon: '👤',
      content: (
        <div>
          <h3>Thông tin cá nhân</h3>
          <p>Tên: Lê Văn Tuân</p>
          <p>Vị trí: HCM, Việt Nam</p>
          <p>Sở thích: Code React, học công nghệ mới, nghe nhạc Lo-fi</p>
        </div>
      ),
    },
    {
      id: 2,
      label: 'Skills',
      icon: '⚡',
      content: (
        <div>
          <h3>Kỹ năng chính</h3>
          <ul style={{ paddingLeft: '20px', lineHeight: '1.8' }}>
            <li>React & React Hooks (đang học nâng cao)</li>
            <li>JavaScript (ES6+)</li>
            <li>HTML5 / CSS3 / Tailwind</li>
            <li>Git & GitHub</li>
            <li>Đang học: TypeScript, Next.js</li>
          </ul>
        </div>
      ),
    },
    {
      id: 3,
      label: 'Projects',
      icon: '🚀',
      content: (
        <div>
          <h3>Dự án nổi bật</h3>
          <div style={{ marginTop: '16px' }}>
            <h4>Todo App React</h4>
            <p>
              Ứng dụng quản lý công việc với thêm/sửa/xóa, filter trạng thái
            </p>
          </div>
          <div style={{ marginTop: '16px' }}>
            <h4>Pricing Table Component</h4>
            <p>Bảng so sánh gói dịch vụ responsive, highlight plan phổ biến</p>
          </div>
        </div>
      ),
    },
    {
      id: 4,
      label: 'Contact',
      icon: '✉️',
      content: (
        <div>
          <h3>Liên hệ với mình</h3>
          <p style={{ margin: '16px 0' }}>
            Email: tuan.dev@example.com
            <br />
            GitHub: github.com/tuandevjs
            <br />
            Twitter/X: @tuan_dev_vn
          </p>
          <button
            style={{
              padding: '12px 28px',
              background: '#3b82f6',
              color: 'white',
              border: 'none',
              borderRadius: '8px',
              fontSize: '16px',
              cursor: 'pointer',
            }}
            onClick={() => alert('Chức năng gửi tin nhắn (demo)')}
          >
            Gửi tin nhắn
          </button>
        </div>
      ),
    },
  ];

  // Hàm xử lý click tab
  const handleTabClick = (tabId) => {
    activeTabId = tabId;
    console.log(`Chuyển sang tab: ${tabs.find((t) => t.id === tabId).label}`);
  };

  // Hàm điều hướng bằng phím mũi tên
  const handleKeyDown = (event) => {
    if (event.key === 'ArrowRight') {
      event.preventDefault();
      const currentIndex = tabs.findIndex((t) => t.id === activeTabId);
      const nextIndex = (currentIndex + 1) % tabs.length;
      activeTabId = tabs[nextIndex].id;
      console.log(`→ Tab tiếp theo: ${tabs[nextIndex].label}`);
    } else if (event.key === 'ArrowLeft') {
      event.preventDefault();
      const currentIndex = tabs.findIndex((t) => t.id === activeTabId);
      const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
      activeTabId = tabs[prevIndex].id;
      console.log(`← Tab trước: ${tabs[prevIndex].label}`);
    }
  };

  // Tìm tab đang active
  const activeTab = tabs.find((tab) => tab.id === activeTabId) || tabs[0];

  return (
    <div
      style={{
        maxWidth: '900px',
        margin: '40px auto',
        padding: '0 20px',
        fontFamily: 'system-ui, sans-serif',
      }}
      tabIndex={0} // Để có thể focus và nhận key events
      onKeyDown={handleKeyDown}
    >
      <h1
        style={{
          textAlign: 'center',
          marginBottom: '40px',
          color: '#1f2937',
        }}
      >
        Tab Navigation Component
      </h1>

      {/* Tab Headers */}
      <div
        role='tablist'
        style={{
          display: 'flex',
          overflowX: 'auto',
          borderBottom: '2px solid #e5e7eb',
          paddingBottom: '4px',
          scrollbarWidth: 'thin',
        }}
      >
        {tabs.map((tab) => (
          <button
            key={tab.id}
            role='tab'
            aria-selected={tab.id === activeTabId}
            tabIndex={tab.id === activeTabId ? 0 : -1}
            onClick={() => handleTabClick(tab.id)}
            style={{
              flex: '0 0 auto',
              padding: '12px 24px',
              fontSize: '1.1rem',
              fontWeight: tab.id === activeTabId ? '700' : '500',
              color: tab.id === activeTabId ? '#3b82f6' : '#4b5563',
              background: 'none',
              border: 'none',
              borderBottom:
                tab.id === activeTabId
                  ? '3px solid #3b82f6'
                  : '3px solid transparent',
              cursor: 'pointer',
              transition: 'all 0.2s',
              whiteSpace: 'nowrap',
            }}
          >
            <span style={{ marginRight: '8px' }}>{tab.icon}</span>
            {tab.label}
          </button>
        ))}
      </div>

      {/* Tab Content */}
      <div
        role='tabpanel'
        style={{
          padding: '32px 24px',
          background: 'white',
          borderRadius: '0 0 12px 12px',
          boxShadow: '0 4px 15px rgba(0,0,0,0.06)',
          minHeight: '300px',
        }}
      >
        {activeTab.content}
      </div>

      {/* Hướng dẫn sử dụng */}
      <div
        style={{
          marginTop: '32px',
          padding: '16px',
          background: '#f3f4f6',
          borderRadius: '8px',
          fontSize: '0.95rem',
          color: '#4b5563',
          textAlign: 'center',
        }}
      >
        <p>
          <strong>Hướng dẫn điều hướng bằng bàn phím:</strong>
          <br />
          Click vào khu vực tab → dùng <kbd>←</kbd> <kbd>→</kbd> để chuyển tab
        </p>
        <p>
          Trạng thái hiện tại: <strong>{activeTab.label}</strong>
        </p>
      </div>
    </div>
  );
}

Các tính năng đã triển khai:

  • 4 tab với nội dung khác nhau (Profile, Skills, Projects, Contact)
  • Tab active có viền dưới xanh + chữ đậm + màu nổi bật
  • Responsive: tab có thể cuộn ngang trên mobile (overflow-x: auto)
  • Keyboard navigation:
    • Focus vào khu vực tab (click hoặc Tab phím)
    • Dùng phím → tab trước
    • Dùng phím → tab sau
  • ARIA roles cơ bản cho accessibility (role="tablist", role="tab", role="tabpanel", aria-selected)
  • Hover & transition nhẹ nhàng
  • Debug hint nhỏ ở dưới để biết tab hiện tại

Hạn chế hiện tại (vì chưa học state):

  • Click tab chỉ log console, không làm UI thay đổi thực sự (vẫn hiển thị tab đầu tiên)
  • Keyboard navigation chỉ log, không chuyển tab trên UI

→ Sau khi học useState (ngày 11), bạn chỉ cần thay let activeTabId = 1 bằng:

jsx
const [activeTabId, setActiveTabId] = useState(1);

và thay mọi activeTabId = ... bằng setActiveTabId(...) là component sẽ hoạt động hoàn hảo, UI cập nhật realtime.


4. Modal with Backdrop Click

Tạo modal component:

  • Button to open modal
  • Click backdrop → close modal
  • Click inside modal content → KHÔNG close
  • Esc key → close modal
  • Prevent body scroll khi modal open
  • Conditional rendering (open/closed states)
💡 Solution - Bài 4 Nâng cao: Modal with Backdrop Click
jsx
// Bài tập về nhà Nâng cao - Ngày 5
// Bài 4: Modal với Backdrop Click
// - Nút mở modal
// - Click backdrop → đóng modal
// - Click bên trong modal content → KHÔNG đóng
// - Nhấn Esc → đóng modal
// - Ngăn scroll body khi modal mở
// - Conditional rendering (hiển thị/ẩn modal)

// =============================================
// Modal Component
// =============================================
function Modal({ isOpen, onClose, title = 'Modal Title', children }) {
  // Xử lý phím Esc để đóng modal
  const handleKeyDown = (event) => {
    if (event.key === 'Escape') {
      onClose();
      console.log('Modal closed by Esc key');
    }
  };

  // Ngăn scroll body khi modal mở
  React.useEffect(() => {
    if (isOpen) {
      // Lưu vị trí scroll hiện tại
      const scrollY = window.scrollY;
      document.body.style.position = 'fixed';
      document.body.style.top = `-${scrollY}px`;
      document.body.style.width = '100%';

      // Thêm event listener cho phím Esc
      window.addEventListener('keydown', handleKeyDown);

      // Cleanup khi đóng modal
      return () => {
        document.body.style.position = '';
        document.body.style.top = '';
        document.body.style.width = '';
        window.scrollTo(0, scrollY);
        window.removeEventListener('keydown', handleKeyDown);
      };
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div
      style={{
        position: 'fixed',
        inset: 0,
        backgroundColor: 'rgba(0, 0, 0, 0.6)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        zIndex: 1000,
        backdropFilter: 'blur(4px)',
      }}
      onClick={onClose} // Click backdrop → đóng
    >
      {/* Modal content - ngăn chặn lan truyền event click */}
      <div
        style={{
          backgroundColor: 'white',
          borderRadius: '12px',
          width: '90%',
          maxWidth: '500px',
          maxHeight: '90vh',
          overflowY: 'auto',
          boxShadow: '0 20px 40px rgba(0,0,0,0.3)',
          position: 'relative',
        }}
        onClick={(e) => e.stopPropagation()} // Ngăn đóng khi click bên trong
      >
        {/* Header */}
        <div
          style={{
            padding: '20px 24px',
            borderBottom: '1px solid #e5e7eb',
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            background: '#f8f9fa',
            borderTopLeftRadius: '12px',
            borderTopRightRadius: '12px',
          }}
        >
          <h2 style={{ margin: 0, fontSize: '1.5rem', color: '#1f2937' }}>
            {title}
          </h2>
          <button
            onClick={onClose}
            aria-label='Đóng modal'
            style={{
              background: 'none',
              border: 'none',
              fontSize: '1.8rem',
              cursor: 'pointer',
              color: '#6b7280',
              padding: '4px 12px',
              borderRadius: '8px',
              transition: 'background 0.2s',
            }}
            onMouseEnter={(e) => (e.currentTarget.style.background = '#fee2e2')}
            onMouseLeave={(e) => (e.currentTarget.style.background = 'none')}
          >
            ×
          </button>
        </div>

        {/* Body */}
        <div style={{ padding: '24px', lineHeight: '1.6' }}>{children}</div>

        {/* Footer (nút hành động tùy chọn) */}
        <div
          style={{
            padding: '16px 24px',
            borderTop: '1px solid #e5e7eb',
            display: 'flex',
            justifyContent: 'flex-end',
            gap: '12px',
            background: '#f8f9fa',
            borderBottomLeftRadius: '12px',
            borderBottomRightRadius: '12px',
          }}
        >
          <button
            onClick={onClose}
            style={{
              padding: '10px 20px',
              background: '#e5e7eb',
              border: 'none',
              borderRadius: '8px',
              cursor: 'pointer',
              fontWeight: '500',
            }}
          >
            Hủy
          </button>
          <button
            onClick={() => {
              console.log('Xác nhận hành động');
              onClose();
            }}
            style={{
              padding: '10px 20px',
              background: '#3b82f6',
              color: 'white',
              border: 'none',
              borderRadius: '8px',
              cursor: 'pointer',
              fontWeight: '500',
            }}
          >
            Xác nhận
          </button>
        </div>
      </div>
    </div>
  );
}

// =============================================
// Demo sử dụng Modal
// =============================================
function App() {
  // Trạng thái modal (sẽ học useState ngày sau)
  // Hiện tại dùng biến thường để demo logic
  let isModalOpen = false;

  const openModal = () => {
    isModalOpen = true;
    console.log('Modal: OPEN');
  };

  const closeModal = () => {
    isModalOpen = false;
    console.log('Modal: CLOSED');
  };

  return (
    <div
      style={{
        minHeight: '100vh',
        background: '#f3f4f6',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        padding: '60px 20px',
        fontFamily: 'system-ui, sans-serif',
      }}
    >
      <h1 style={{ marginBottom: '40px', color: '#1f2937' }}>
        Modal with Backdrop Click Demo
      </h1>

      <button
        onClick={openModal}
        style={{
          padding: '14px 32px',
          fontSize: '1.2rem',
          background: '#3b82f6',
          color: 'white',
          border: 'none',
          borderRadius: '10px',
          cursor: 'pointer',
          boxShadow: '0 4px 12px rgba(59,130,246,0.25)',
          transition: 'all 0.2s',
        }}
        onMouseEnter={(e) => {
          e.currentTarget.style.transform = 'translateY(-2px)';
          e.currentTarget.style.boxShadow = '0 8px 20px rgba(59,130,246,0.35)';
        }}
        onMouseLeave={(e) => {
          e.currentTarget.style.transform = 'translateY(0)';
          e.currentTarget.style.boxShadow = '0 4px 12px rgba(59,130,246,0.25)';
        }}
      >
        Mở Modal
      </button>

      <p style={{ marginTop: '24px', color: '#4b5563', textAlign: 'center' }}>
        Hướng dẫn sử dụng:
        <br />
        • Click nút "Mở Modal"
        <br />
        • Click backdrop (vùng tối) → đóng
        <br />
        • Click nút × hoặc "Hủy" → đóng
        <br />
        • Nhấn phím Esc → đóng
        <br />• Click bên trong modal → không đóng
      </p>

      {/* Modal xuất hiện khi isModalOpen = true */}
      <Modal
        isOpen={isModalOpen}
        onClose={closeModal}
        title='Xác nhận hành động'
      >
        <p style={{ margin: '0 0 16px 0' }}>
          Bạn có chắc chắn muốn thực hiện hành động này không?
        </p>
        <p style={{ color: '#6b7280', fontSize: '0.95rem' }}>
          Lưu ý: Hành động này không thể hoàn tác sau khi xác nhận.
        </p>

        {/* Nội dung tùy ý có thể thêm vào đây */}
        <div
          style={{
            margin: '24px 0',
            padding: '16px',
            background: '#f3f4f6',
            borderRadius: '8px',
          }}
        >
          <h4>Thông tin chi tiết</h4>
          <ul style={{ margin: '12px 0 0 20px' }}>
            <li>Thời gian: {new Date().toLocaleString('vi-VN')}</li>
            <li>Vị trí: Đồng Nai, Việt Nam</li>
            <li>Ngày hiện tại: 19/01/2026</li>
          </ul>
        </div>
      </Modal>

      {/* Hiển thị trạng thái debug */}
      <div
        style={{
          marginTop: '40px',
          padding: '12px 24px',
          background: '#e5e7eb',
          borderRadius: '8px',
          fontSize: '0.95rem',
          color: '#374151',
        }}
      >
        Trạng thái modal hiện tại:
        <strong style={{ color: isModalOpen ? '#3b82f6' : '#ef4444' }}>
          {isModalOpen ? 'ĐANG MỞ' : 'ĐÃ ĐÓNG'}
        </strong>
        {' • '}Kiểm tra console khi mở/đóng
      </div>
    </div>
  );
}

export default App;

Các tính năng đã triển khai:

  • Mở modal bằng nút
  • Đóng modal bằng:
    • Click backdrop (vùng tối xung quanh)
    • Nút × ở góc
    • Nút "Hủy" ở footer
    • Phím Esc
  • Ngăn click lan truyền: e.stopPropagation() khi click bên trong modal content
  • Ngăn scroll body khi modal mở (dùng position: fixed + lưu vị trí scroll)
  • Khôi phục scroll chính xác khi đóng modal
  • Keyboard accessibility cơ bản (Esc để đóng)
  • Responsive và đẹp mắt trên mọi thiết bị
  • Debug hint nhỏ để xem trạng thái

Hạn chế hiện tại (vì chưa học state):

  • Click nút chỉ log console, modal không thực sự mở/đóng trên UI (vẫn ẩn)
  • Biến isOpen không persist → không re-render

→ Sau khi học useState (ngày 11), bạn chỉ cần thay:

jsx
let isModalOpen = false;
const openModal = () => {
  isModalOpen = true;
};
const closeModal = () => {
  isModalOpen = false;
};

bằng:

jsx
const [isModalOpen, setIsModalOpen] = useState(false);
const openModal = () => setIsModalOpen(true);
const closeModal = () => setIsModalOpen(false);

là modal sẽ hoạt động hoàn hảo, UI cập nhật realtime.

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - Responding to Eventshttps://react.dev/learn/responding-to-events

  2. React Docs - Conditional Renderinghttps://react.dev/learn/conditional-rendering

Đọc thêm

  1. MDN - Event Referencehttps://developer.mozilla.org/en-US/docs/Web/Events

  2. React Docs - SyntheticEventhttps://react.dev/reference/react-dom/components/common#react-event-object


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

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

  • Ngày 1-2: ES6+ (arrow functions, ternary operator)
  • Ngày 3: JSX (expressions in curly braces)
  • Ngày 4: Components & Props (passing event handlers as props)

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

  • Ngày 6: Lists & Keys (map + event handlers for each item)
  • Ngày 7: Component Composition (passing handlers through composition)
  • Ngày 11: useState (FINALLY update UI on events!)
  • Ngày 13: Forms with State (controlled components)

💡 SENIOR INSIGHTS

Cân nhắc khi làm Production

1. Hiệu năng của Event Handler:

jsx
// ❌ KHÔNG TỐT: Tạo function mới mỗi lần render
function List({ items }) {
  return items.map((item) => (
    <div onClick={() => handleClick(item.id)}>{item.name}</div>
  ));
}

// ✅ TỐT HƠN: Tái sử dụng handler (sẽ học tối ưu sâu hơn ở Day 31)
function List({ items }) {
  const handleClick = (id) => () => {
    console.log('Clicked:', id);
  };

  return items.map((item) => (
    <div onClick={handleClick(item.id)}>{item.name}</div>
  ));
}

2. Accessibility (Khả năng truy cập):

jsx
// ❌ KHÔNG TỐT: div có onClick, không hỗ trợ bàn phím
<div onClick={handleClick}>Click me</div>

// ✅ TỐT: Dùng button, tự động hỗ trợ bàn phím
<button onClick={handleClick}>Click me</button>

// ✅ TỐT: Nếu buộc phải dùng div, cần bổ sung hỗ trợ bàn phím
<div
  role="button"      // khai báo vai trò là button
  tabIndex={0}       // cho phép focus bằng bàn phím
  onClick={handleClick}
  onKeyPress={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      handleClick();
    }
  }}
>
  Click me
</div>

3. Event Delegation (React xử lý nội bộ):

React tự động áp dụng event delegation — chỉ gắn một event listener ở root (thường là document), thay vì gắn listener cho từng element.

Điều này có nghĩa là:

  • Không tạo quá nhiều event listener cho mỗi component
  • Giảm chi phí bộ nhớ và cải thiện hiệu năng
  • Event được “bắt” ở root rồi phân phối ngược lại component tương ứng

➡️ Bạn không cần tự xử lý event delegation, nhưng nên biết để:

  • Hiểu vì sao React xử lý event hiệu quả
  • Dễ debug các vấn đề liên quan đến bubbling / capturing
  • Có tư duy performance tốt hơn khi làm production

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

Junior Level:

Q1: "Khác biệt giữa onClick={func}onClick={func()}?"
A: onClick={func} pass function reference, execute khi click. onClick={func()} execute ngay lập tức khi render, pass kết quả (thường là undefined) vào onClick.

Q2: "SyntheticEvent là gì?"
A: Là wrapper của React quanh native browser events, đảm bảo consistent API across browsers và performance optimization.

Mid Level:

Q3: "Làm sao pass arguments vào event handler?"
A: Dùng arrow function: onClick={() => handleClick(id)} hoặc higher-order function: onClick={handleClick(id)} với const handleClick = (id) => (event) => {...}

Q4: "Khi nào dùng event.stopPropagation()?"
A: Khi muốn prevent event bubbling lên parent elements. VD: Click button inside clickable card, chỉ trigger button handler, không trigger card handler.

Senior Level:

Q5: “Conditional rendering pattern nào tốt nhất cho performance?”

A: Tùy vào use case, không có pattern nào “best” cho mọi trường hợp.

  • If / Else với early return Phù hợp cho logic phức tạp, nhiều nhánh. Code dễ đọc, dễ tối ưu và tránh render không cần thiết.

  • Ternary (? :) Tốt cho toggle đơn giản (true / false). Gọn, rõ ràng nếu không lồng nhiều tầng.

  • Logical && Chỉ nên dùng để ẩn/hiện component. Cẩn thận với các giá trị falsy như 0, '', null.

  • Tránh nested ternaries Dễ gây khó đọc, khó maintain và dễ sai logic hơn là vấn đề performance.

👉 Kết luận: Performance khác biệt giữa các pattern thường rất nhỏ; độ rõ ràng và khả năng bảo trì quan trọng hơn.


War Stories

Story 1: The preventDefault Bug

"Production bug nghiêm trọng: form submit reload page, mất data user đang nhập. Root cause: Junior dev quên event.preventDefault() trong submit handler. Mất 3 giờ debug vì chỉ xảy ra randomly (user bấm Enter vs Click button). Lesson: ALWAYS preventDefault cho forms!"

Story 2: The && Operator Gotcha

"UI bug trên dashboard: số '0' hiển thị khi không có data. QA report: 'Why showing zero when should be empty?' Code: {count && <Stats count={count} />}. Fix: {count > 0 && ...}. Giờ luôn review PRs để catch pattern này!"

Story 3: The Inline Arrow Performance

"App chạy chậm khi scroll list 1000 items. Profiler shows: re-render storm. Cause: onClick={() => handleClick(item.id)} tạo mới function mỗi render, trigger re-render children. Fix: Memoization (sẽ học Day 34). Nhưng nhớ: Premature optimization is evil - measure first!"


🎯 PREVIEW NGÀY MAI

Ngày 6: Lists & Keys - Rendering Arrays

Tomorrow chúng ta sẽ học:

  • Rendering arrays với .map()
  • Keys trong React - tại sao quan trọng
  • Index as key - khi nào OK, khi nào KHÔNG
  • Stable keys strategies
  • Common list patterns

Chuẩn bị:

  • Review array methods: .map(), .filter(), .sort()
  • Ôn lại Events (sẽ kết hợp với lists)
  • Nghĩ về các UI components render lists (todos, products, comments...)

Sneak peek:

jsx
// Tomorrow sẽ học pattern này:
const todos = [
  { id: 1, text: 'Learn Lists' },
  { id: 2, text: 'Master Keys' },
];

return (
  <ul>
    {todos.map((todo) => (
      <li key={todo.id}>
        {todo.text}
        <button onClick={() => handleDelete(todo.id)}>Delete</button>
      </li>
    ))}
  </ul>
);

🎊 CHÚC MỪNG!

Bạn đã hoàn thành Ngày 5: Events & Conditional Rendering!

Hôm nay bạn đã học:

✅ Event handling trong React (declarative approach)
✅ SyntheticEvents & cross-browser compatibility
✅ Event binding patterns & passing arguments
✅ Conditional rendering với if/else, ternary, &&
✅ Common patterns & anti-patterns
✅ Real-world interactive components

Key Takeaways (Tóm tắt quan trọng):

  • Dùng onClick={func} không phải onClick={func()} ⚠️
  • Luôn gọi event.preventDefault() khi xử lý form submit
  • Cẩn thận khi dùng && với các giá trị falsy (đặc biệt là 0) ⚠️
  • Chọn pattern conditional rendering phù hợp để code dễ đọc, dễ maintain

Next steps:

  1. Hoàn thành bài tập về nhà
  2. Practice event handlers với keyboard events
  3. Thử build một component với nhiều states
  4. Chuẩn bị cho Ngày 6: Lists & Keys!

Keep coding! 💪 See you tomorrow! 🚀

Personal tech knowledge base