📅 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:
- Props là gì? Nó read-only hay có thể modify?
- Arrow function syntax như thế nào?
() => {}khác gì vớifunction() {}? - Ternary operator hoạt động ra sao? Cho ví dụ.
💡 Solution
Props là data truyền từ parent → child. Props là read-only (immutable), child không được sửa props.
Arrow function:
// 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- Ternary operator:
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ý:
<!-- 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ự
addEventListenervàremoveEventListener, 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.
// 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 compatibleLợ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 functionKhi 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):
button.addEventListener('click', (e) => {
console.log(e instanceof MouseEvent); // true
});SyntheticEvent (React):
<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):
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:
setTimeout(() => {
console.log(e.target.value); // ❌ e.target === null
}, 1000);Xử lý bất đồng bộ (React 17+):
<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):
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ý asyncReact 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"
// ❌ 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"
// ❌ 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
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
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ểm | Nhược điểm | Khi 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 render | Khi cần truyền tham số |
💡 Alternative: Higher-Order Function
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 ⭐⭐
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ện | Khi nào được kích hoạt | Trường hợp sử dụng |
|---|---|---|
onClick | Khi click vào phần tử | Button, link |
onChange | Khi giá trị input thay đổi | Form, input |
onSubmit | Khi form được submit | Xử lý form |
onKeyDown | Khi nhấn phím | Phím tắt, shortcut |
onKeyUp | Khi nhả phím | Validate dữ liệu nhập |
onFocus | Khi phần tử được focus | Highlight input |
onBlur | Khi phần tử mất focus | Validate khi blur |
onMouseEnter | Khi chuột đi vào phần tử | Hiệu ứng hover |
onMouseLeave | Khi chuột rời khỏi phần tử | Ẩn tooltip |
Demo 3: Event Delegation (Ủy quyền) & Bubbling (Nổi Bọt) ⭐⭐⭐
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 chaKhi 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 ⭐
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 ⭐⭐
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!
// ❌ 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 ⭐⭐⭐
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
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:
false0⚠️ renders as "0"""(empty string)nullundefinedNaN
Demo 7: Multiple Conditions Pattern ⭐⭐⭐
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:
- Button "Increment" - log "Count: 1", "Count: 2"...
- Button "Decrement" - log "Count: -1", "Count: -2"...
- Button "Reset" - log "Count reset to 0"
- Hiển thị message dựa trên count (dùng conditional rendering concept)
/**
* 💡 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
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
countkhô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:
- Email input với onChange validation
- Password input với visibility toggle
- Submit button disabled nếu invalid
- Show error messages conditionally
// 🎯 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
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
// 🎯 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
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:
.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):
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: ?
Phát hiện tổ hợp phím:
- Làm sao phân biệt
Cmd + Kvới chỉ nhấnK? - Xử lý khác nhau giữa
Ctrl(Windows) vàCmd(Mac) như thế nào?
- Làm sao phân biệt
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):
## 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
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:
.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.ctrlKeykết hợp vớievent.key === 'k'để mở/đóng Command Palette bằngCmd/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 đổiselectedIndex, 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
commandstheosearchQuery, 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
isOpenlàtrue.Command execution logic Nhấn
Enterhoặc click vào command sẽ thực thi hành động tương ứng và đóng palette.Lưu ý quan trọng
isOpen,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ằnguseStatevà global listener cần gắn/gỡ bằnguseEffect.
⭐⭐⭐⭐⭐ 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
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>
);
}.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:
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
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
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
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
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
| Pattern | Ví dụ code | Ưu điểm ✅ | Nhược điểm ❌ | Khi nên dùng |
|---|---|---|---|---|
| Function Reference | onClick={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 Function | onClick={() => 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 Function | onClick={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 Method | onClick={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
| Pattern | Cú pháp | Ưu điểm ✅ | Nhược điểm ❌ | Phù hợp khi |
|---|---|---|---|---|
| If / Else | if (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 |
| Switch | switch (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 Map | const 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 clausesDecision 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) ❌
// 🐛 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:
- Lỗi gì xảy ra?
- Tại sao function chạy ngay?
- 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:
onClick={increment()} // ❌ Calls function immediately
// ^^ parentheses execute the function NOW
// What React sees:
onClick={undefined} // Result of increment() call3. Fix:
// ✅ 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 referenceonClick={func()}→ execute now ❌onClick={() => func()}→ execute on click ✅
Bug 2: Missing preventDefault ❌
// 🐛 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:
- Chuyện gì xảy ra khi submit form?
- Tại sao console.log không thấy?
- 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:
// ✅ 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 ❌
// 🐛 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:
- Output là gì khi
items = []? - Tại sao xuất hiện số "0" trên screen?
- Làm sao fix?
💡 Giải đáp
1. Output:
<div>
<h2>Shopping Cart</h2>
0
<!-- ❌ Oops! Number 0 is rendered -->
<p>Your cart is empty</p>
</div>2. Tại sao:
{
itemCount && <Component />;
}
// When itemCount = 0:
{
0 && <Component />;
}
// Evaluates to: 0
// React renders: 0JavaScript falsy values:
false→ not renderednull→ not renderedundefined→ not rendered""→ not rendered0→ RENDERED as "0" ⚠️NaN→ rendered as "NaN"
3. Fix:
// ❌ 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}vsonClick={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)
// 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
// 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ọcuseState(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
// 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:
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
// 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:
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
// 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
isOpenkhông persist → không re-render
→ Sau khi học useState (ngày 11), bạn chỉ cần thay:
let isModalOpen = false;
const openModal = () => {
isModalOpen = true;
};
const closeModal = () => {
isModalOpen = false;
};bằng:
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
React Docs - Responding to Eventshttps://react.dev/learn/responding-to-events
React Docs - Conditional Renderinghttps://react.dev/learn/conditional-rendering
Đọc thêm
MDN - Event Referencehttps://developer.mozilla.org/en-US/docs/Web/Events
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:
// ❌ 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):
// ❌ 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} và 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:
// 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ảionClick={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:
- Hoàn thành bài tập về nhà
- Practice event handlers với keyboard events
- Thử build một component với nhiều states
- Chuẩn bị cho Ngày 6: Lists & Keys!
Keep coding! 💪 See you tomorrow! 🚀