📅 NGÀY 3: REACT BASICS & JSX - BƯỚC ĐẦU VÀO THỂ GIỚI REACT
📍 Phase 1, Week 1, Day 3 of 75
⏱️ Thời lượng: 3-4 giờ (bao gồm breaks)
🎯 Mục tiêu học tập (5 phút)
Sau bài học hôm nay, bạn sẽ:
- [ ] Hiểu React là gì và tại sao nó được sử dụng rộng rãi
- [ ] Tạo được React app đầu tiên và hiểu cấu trúc project
- [ ] Thành thạo JSX syntax - cách viết UI với JavaScript
- [ ] Phân biệt rõ JSX vs HTML và biết khi nào dùng gì
- [ ] Nhúng JavaScript expressions vào JSX một cách chính xác
🤔 Kiểm tra đầu vào (5 phút)
Trước khi bắt đầu, hãy tự hỏi mình:
- Arrow functions:
const greet = (name) => <h1>Hello {name}</h1>- Bạn hiểu syntax này chứ? - Destructuring:
const { name, age } = props- Sẽ dùng nhiều trong React! - Template literals: Có khác gì với JSX expressions không?
💡 Nếu chưa vững về ES6 (Ngày 1-2), hãy ôn lại trước!
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
Scenario: Xây dựng UI truyền thống với vanilla JavaScript
<!-- ❌ CÁCH CŨ: Vanilla JavaScript + HTML -->
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script>
// Tạo UI element by element
const root = document.getElementById('root');
const heading = document.createElement('h1');
heading.textContent = 'Welcome to My App';
heading.className = 'title';
const button = document.createElement('button');
button.textContent = 'Click me';
button.onclick = function () {
alert('Clicked!');
};
const container = document.createElement('div');
container.className = 'container';
container.appendChild(heading);
container.appendChild(button);
root.appendChild(container);
// 😱 Imagine doing this for ENTIRE APP!
</script>
</body>
</html>Vấn đề:
- Code dài dòng - Tạo từng element rất verbose
- Khó hình dung UI - Không thấy được structure
- Khó maintain - Update UI phức tạp
- Khó tái sử dụng - Copy-paste code nhiều
- Performance issues - Manual DOM manipulation chậm
1.2 Giải Pháp: React & JSX
React giải quyết những vấn đề trên bằng cách:
✅ Declarative UI - Mô tả UI muốn có, React lo việc render
✅ Component-based - Chia nhỏ UI thành các pieces tái sử dụng
✅ JSX Syntax - Viết UI giống HTML ngay trong JavaScript
✅ Virtual DOM - Optimize performance tự động
✅ Ecosystem - Tools, libraries, community lớn
Cùng code với React:
// ✅ CÁCH MỚI: React + JSX
import React from 'react';
function App() {
const handleClick = () => {
alert('Clicked!');
};
return (
<div className='container'>
<h1 className='title'>Welcome to My App</h1>
<button onClick={handleClick}>Click me</button>
</div>
);
}
// 🎉 Gọn gàng, dễ đọc, dễ maintain!1.3 Mental Model
┌───────────────────────────────────────────────────────┐
│ REACT ECOSYSTEM │
├───────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ REACT CORE │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ Components (Functions) │ │ │
│ │ │ - Return JSX │ │ │
│ │ │ - Reusable pieces │ │ │
│ │ └────────────────────────────────┘ │ │
│ │ ↓ │ │
│ │ ┌───────────────────────────────────────┐ │ │
│ │ │ JSX (Syntax Extension) │ │ │
│ │ │ - HTML-like in JavaScript │ │ │
│ │ │ - Compiled to React.createElement │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ │ ↓ │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ Virtual DOM │ │ │
│ │ │ - In-memory representation │ │ │
│ │ │ - Diff & Update efficiently │ │ │
│ │ └────────────────────────────────┘ │ │
│ │ ↓ │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ Real DOM │ │ │
│ │ │ - Browser updates │ │ │
│ │ │ - User sees UI │ │ │
│ │ └────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────┘Analogy dễ hiểu:
🏗️ Xây nhà:
- Vanilla JS = Đặt từng viên gạch thủ công (tedious!)
- React Components = Lắp ráp các panels đã làm sẵn (efficient!)
- JSX = Bản thiết kế (blueprint) dễ đọc
- Virtual DOM = Mô hình 3D giúp tối ưu việc xây
1.4 Hiểu Lầm Phổ Biến
❌ SAI #1: "JSX là template engine như Handlebars, Pug"
✅ ĐÚNG: JSX là JavaScript. Mọi thứ trong JSX đều là JavaScript code
❌ SAI #2: "JSX là HTML"
✅ ĐÚNG: JSX trông như HTML nhưng có khác biệt (className, onClick, style object...)
❌ SAI #3: "Phải dùng JSX với React"
✅ ĐÚNG: JSX là optional, nhưng 99.9% React devs dùng JSX vì tiện
❌ SAI #4: "React nặng và chậm"
✅ ĐÚNG: React được optimize cho performance, Virtual DOM giúp cập nhật hiệu quả
💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Hello React - First Component ⭐
A. Setup React App
# Cách 1: Create React App (CRA) - Recommended for learning
npx create-react-app my-first-app
cd my-first-app
npm start
# Cách 2: Vite (Faster, modern) - Also good
npm create vite@latest my-first-app -- --template react
cd my-first-app
npm install
npm run devCấu trúc project sau khi tạo:
my-first-app/
├── node_modules/ # Dependencies
├── public/ # Static files
│ └── index.html # HTML template
├── src/
│ ├── App.js # Root component ⭐ FOCUS HERE
│ ├── index.js # Entry point
│ └── index.css # Global styles
├── package.json # Project config
└── README.mdB. Your First Component
// ========================================
// FILE: src/App.js
// ========================================
// ✅ STEP 1: Import React (trong React 17+ không bắt buộc nhưng nên có)
import React from 'react';
// ✅ STEP 2: Define Component (Function Component)
function App() {
// ✅ STEP 3: Return JSX
return (
<div>
<h1>Hello React!</h1>
<p>This is my first React component</p>
</div>
);
}
// ✅ STEP 4: Export Component
export default App;
// 🎓 ANATOMY OF A COMPONENT:
// 1. Function name = Component name (PascalCase)
// 2. Returns JSX (looks like HTML)
// 3. Export để dùng ở nơi khácGiải thích chi tiết:
// ========================================
// COMPARING: React Element vs HTML Element
// ========================================
// HTML Element (DOM API):
const element = document.createElement('h1');
element.textContent = 'Hello';
element.className = 'title';
// React Element (JSX):
const element = <h1 className='title'>Hello</h1>;
// 🔍 BEHIND THE SCENES:
// JSX được compile thành:
const element = React.createElement(
'h1', // type
{ className: 'title' }, // props
'Hello' // children
);
// ⚠️ BẠN KHÔNG CẦN VIẾT React.createElement!
// Chỉ cần viết JSX, compiler lo việc convertC. JSX Expressions - Nhúng JavaScript
// ========================================
// DEMO: JavaScript Expressions trong JSX
// ========================================
function WelcomeMessage() {
// Variables
const userName = 'John Doe';
const currentYear = 2024;
// Functions
const getGreeting = () => {
const hour = new Date().getHours();
if (hour < 12) return 'Good morning';
if (hour < 18) return 'Good afternoon';
return 'Good evening';
};
// Objects
const user = {
name: 'Jane',
age: 25,
role: 'Developer',
};
return (
<div>
{/* ✅ CÁCH 1: Simple variable */}
<h1>Welcome, {userName}!</h1>
{/* ✅ CÁCH 2: Expression */}
<p>Current year: {currentYear}</p>
<p>Next year: {currentYear + 1}</p>
{/* ✅ CÁCH 3: Function call */}
<p>
{getGreeting()}, {userName}
</p>
{/* ✅ CÁCH 4: Object property access */}
<p>
Hello {user.name}, you are {user.age} years old
</p>
{/* ✅ CÁCH 5: Ternary operator */}
<p>You are {user.age >= 18 ? 'an adult' : 'a minor'}</p>
{/* ✅ CÁCH 6: Logical AND */}
{user.role === 'Developer' && <span>👨💻 Developer Badge</span>}
{/* ✅ CÁCH 7: Template literal */}
<p>{`${user.name} is a ${user.role}`}</p>
{/* ❌ KHÔNG ĐƯỢC: Statements (if, for, while) */}
{/* {if (user.age > 18) { return <p>Adult</p> }} */}
{/* ❌ KHÔNG ĐƯỢC: Object trực tiếp */}
{/* <p>{user}</p> */}
{/* Error: Objects are not valid as a React child */}
</div>
);
}
// 🎓 RULES FOR EXPRESSIONS:
// ✅ Can use: Variables, functions, ternary, logical operators
// ✅ Must return: String, Number, JSX, Array of JSX
// ❌ Cannot use: Statements (if, for, switch)
// ❌ Cannot render: Objects directly (except JSX objects)📊 So sánh Template Literal vs JSX Expression:
// Template Literal (ES6)
const html = `
<div>
<h1>Hello ${name}</h1>
<p>Age: ${age}</p>
</div>
`;
// Returns: String
// JSX Expression (React)
const jsx = (
<div>
<h1>Hello {name}</h1>
<p>Age: {age}</p>
</div>
);
// Returns: React Element (Virtual DOM node)Demo 2: JSX vs HTML - Key Differences ⭐⭐
// ========================================
// DEMO: JSX vs HTML Differences
// ========================================
function JSXDifferencesDemo() {
const imageUrl = 'https://via.placeholder.com/150';
const inputValue = 'Hello';
return (
<div>
<h2>JSX vs HTML Differences</h2>
{/* ========================================
DIFFERENCE 1: className vs class
========================================*/}
{/* ❌ HTML: */}
{/* <div class="container"></div> */}
{/* ✅ JSX: */}
<div className='container'>
{/* class là reserved keyword trong JavaScript */}
{/* JSX dùng className thay vì class */}
</div>
{/* ========================================
DIFFERENCE 2: htmlFor vs for
========================================*/}
{/* ❌ HTML: */}
{/* <label for="email">Email</label> */}
{/* ✅ JSX: */}
<label htmlFor='email'>Email:</label>
<input
id='email'
type='text'
/>
{/* ========================================
DIFFERENCE 3: Self-closing tags
========================================*/}
{/* ❌ HTML: Can omit closing */}
{/* <img src="..."> */}
{/* <br> */}
{/* <input type="text"> */}
{/* ✅ JSX: MUST self-close */}
<img
src={imageUrl}
alt='Placeholder'
/>
<br />
<input type='text' />
{/* ========================================
DIFFERENCE 4: style attribute
========================================*/}
{/* ❌ HTML: String */}
{/* <div style="color: red; font-size: 20px;"></div> */}
{/* ✅ JSX: Object with camelCase */}
<div
style={{
color: 'red', // camelCase
fontSize: '20px', // fontSize not font-size
backgroundColor: 'blue', // backgroundColor not background-color
marginTop: '10px', // marginTop not margin-top
}}
>
Styled text
</div>
{/* 💡 TIP: Double braces {{ }} */}
{/* Outer {} = JSX expression */}
{/* Inner {} = JavaScript object */}
{/* ========================================
DIFFERENCE 5: Event handlers
========================================*/}
{/* ❌ HTML: String, lowercase */}
{/* <button onclick="handleClick()">Click</button> */}
{/* ✅ JSX: Function reference, camelCase */}
<button onClick={() => alert('Clicked!')}>Click me</button>
{/* ========================================
DIFFERENCE 6: Comments
========================================*/}
{/* ❌ HTML comment: */}
{/* <!-- This is HTML comment --> */}
{/* ✅ JSX comment (inside JSX): */}
{/* This is JSX comment */}
{/* ✅ JavaScript comment (outside JSX): */}
// This is JS comment
{/* ========================================
DIFFERENCE 7: Boolean attributes
========================================*/}
{/* ❌ HTML: */}
{/* <input disabled> */}
{/* <input checked> */}
{/* ✅ JSX: Explicit boolean */}
<input
type='checkbox'
disabled={true}
/>
<input
type='checkbox'
checked={false}
/>
{/* 💡 Shorthand (same as disabled={true}): */}
<input
type='text'
disabled
/>
{/* ========================================
DIFFERENCE 8: Multiple root elements
========================================*/}
{/* ❌ INVALID: Multiple root elements */}
{/*
return (
<h1>Title</h1>
<p>Paragraph</p>
);
*/}
{/* ✅ VALID: Wrap in single parent */}
{/*
return (
<div>
<h1>Title</h1>
<p>Paragraph</p>
</div>
);
*/}
{/* ✅ ALSO VALID: React Fragment */}
{/*
return (
<>
<h1>Title</h1>
<p>Paragraph</p>
</>
);
*/}
</div>
);
}
export default JSXDifferencesDemo;📊 Quick Reference Table:
| Feature | HTML | JSX | Reason |
|---|---|---|---|
| Class | class="..." | className="..." | class is JS reserved word |
| Label For | for="..." | htmlFor="..." | for is JS reserved word |
| Self-closing | Optional | Required | XML syntax requirement |
| Style | String | Object | Type-safe styling |
| Events | onclick | onClick | camelCase convention |
| Comments | <!-- --> | {/* */} | Inside JS expressions |
| Boolean attrs | disabled | disabled={true} | Explicit boolean |
| Multiple roots | Allowed | Need wrapper | Single root rule |
Demo 3: JSX Edge Cases & Best Practices ⭐⭐⭐
// ========================================
// DEMO: JSX Edge Cases
// ========================================
function JSXEdgeCases() {
const items = ['Apple', 'Banana', 'Cherry'];
const emptyArray = [];
const user = { name: 'John', age: 25 };
const nullValue = null;
const undefinedValue = undefined;
const booleanValue = true;
const numberValue = 0;
return (
<div>
<h2>JSX Edge Cases</h2>
{/* ========================================
CASE 1: Rendering null, undefined, boolean
========================================*/}
<p>Null: {nullValue}</p>
{/* Renders: Null: (nothing) */}
<p>Undefined: {undefinedValue}</p>
{/* Renders: Undefined: (nothing) */}
<p>Boolean: {booleanValue}</p>
{/* Renders: Boolean: (nothing) */}
<p>Number zero: {numberValue}</p>
{/* Renders: Number zero: 0 */}
{/* 🎓 RULE: null, undefined, boolean are ignored */}
{/* Only numbers are rendered (including 0) */}
{/* ========================================
CASE 2: Rendering arrays
========================================*/}
{/* ✅ Array of strings/numbers */}
<p>Items: {items}</p>
{/* Renders: Items: AppleBananaCherry */}
{/* ✅ Array of JSX elements */}
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
{/* ⚠️ Empty array */}
<p>Empty: {emptyArray}</p>
{/* Renders: Empty: (nothing) */}
{/* ========================================
CASE 3: Conditional rendering patterns
========================================*/}
{/* ✅ PATTERN 1: Ternary */}
{items.length > 0 ? <p>We have {items.length} items</p> : <p>No items</p>}
{/* ✅ PATTERN 2: Logical AND */}
{items.length > 0 && <p>Items available!</p>}
{/* ❌ PITFALL: Falsy number */}
{items.length && <p>Has items</p>}
{/* If items.length = 0, renders "0" */}
{/* ✅ FIX: Explicit boolean */}
{items.length > 0 && <p>Has items</p>}
{/* ✅ PATTERN 3: Nullish coalescing for defaults */}
<p>Count: {items.length ?? 'N/A'}</p>
{/* ========================================
CASE 4: Rendering objects
========================================*/}
{/* ❌ INVALID: Direct object rendering */}
{/* <p>User: {user}</p> */}
{/* Error: Objects are not valid as a React child */}
{/* ✅ VALID: Access properties */}
<p>
User: {user.name}, Age: {user.age}
</p>
{/* ✅ VALID: JSON stringify for debugging */}
<pre>{JSON.stringify(user, null, 2)}</pre>
{/* ========================================
CASE 5: Whitespace handling
========================================*/}
{/* JSX removes whitespace between lines */}
<p>Hello World</p>
{/* Renders: HelloWorld (no space!) */}
{/* ✅ FIX 1: Explicit space */}
<p>Hello World</p>
{/* ✅ FIX 2: Template literal */}
<p>{`Hello World`}</p>
{/* ✅ FIX 3: Multiple strings */}
<p>
{'Hello'} {'World'}
</p>
{/* ========================================
CASE 6: Reserved words
========================================*/}
{/* ❌ INVALID: JavaScript reserved words */}
{/* <div class="box"></div> */}
{/* <label for="name"></label> */}
{/* ✅ VALID: Use JSX equivalents */}
<div className='box'></div>
<label htmlFor='name'></label>
{/* ========================================
CASE 7: Escape HTML entities
========================================*/}
{/* ❌ Will render as text: */}
<p><div> tag</p>
{/* Renders: <div> tag (literally) */}
{/* ✅ Use actual characters: */}
<p>{'<div>'} tag</p>
{/* Renders: <div> tag */}
{/* ✅ Or use Unicode: */}
<p>
{'\u003C'}div{'\u003E'} tag
</p>
</div>
);
}
// ========================================
// BEST PRACTICES
// ========================================
function BestPractices() {
const isLoggedIn = true;
const userRole = 'admin';
const items = [1, 2, 3];
return (
<div>
{/* ✅ PRACTICE 1: Explicit conditionals */}
{isLoggedIn === true && <p>Welcome back!</p>}
{/* Better than: {isLoggedIn && ...} */}
{/* ✅ PRACTICE 2: Guard against 0 */}
{items.length > 0 && <p>Has items</p>}
{/* NOT: {items.length && ...} */}
{/* ✅ PRACTICE 3: Use fragments for no extra DOM */}
<>
<h1>Title</h1>
<p>Content</p>
</>
{/* Instead of: <div>...</div> */}
{/* ✅ PRACTICE 4: Extract complex expressions */}
{(() => {
if (userRole === 'admin') return <p>Admin Panel</p>;
if (userRole === 'user') return <p>User Dashboard</p>;
return <p>Guest View</p>;
})()}
{/* ✅ PRACTICE 5: Use variables for readability */}
{(() => {
const isAdmin = userRole === 'admin';
const isUser = userRole === 'user';
return (
<>
{isAdmin && <p>Admin Panel</p>}
{isUser && <p>User Dashboard</p>}
</>
);
})()}
{/* ✅ PRACTICE 6: Comments for complex logic */}
{/*
Show admin panel only for:
- Logged in users
- With admin role
- On weekdays
*/}
{isLoggedIn && userRole === 'admin' && <p>Admin Panel</p>}
</div>
);
}🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Exercise 1: JSX Basics (15 phút)
/**
* 🎯 Mục tiêu: Tạo React component đầu tiên với JSX
* ⏱️ Thời gian: 15 phút
* 🚫 KHÔNG dùng: Props, State, Events (chưa học!)
*
* Requirements:
* 1. Tạo function component tên "UserProfile"
* 2. Hiển thị thông tin user với JSX
* 3. Sử dụng JavaScript expressions
* 4. Follow JSX rules (className, style object, etc.)
*
* 💡 Gợi ý: Sử dụng variables và expressions
*/
// ❌ CÁCH SAI: Hardcode values
function UserProfileWrong() {
return (
<div>
<h1>John Doe</h1>
<p>Age: 25</p>
<p>Email: john@example.com</p>
</div>
);
// Problem: Không flexible, phải edit JSX mỗi lần change data
}
// ✅ CÁCH ĐÚNG: Use variables và expressions
function UserProfileCorrect() {
// Define data
const user = {
firstName: 'John',
lastName: 'Doe',
age: 25,
email: 'john@example.com',
role: 'Developer',
isActive: true,
};
// Calculated values
const fullName = `${user.firstName} ${user.lastName}`;
const birthYear = new Date().getFullYear() - user.age;
return (
<div className='user-profile'>
{/* Use expressions */}
<h1>{fullName}</h1>
<p>
Age: {user.age} (Born in {birthYear})
</p>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
{/* Conditional rendering */}
<p>Status: {user.isActive ? '🟢 Active' : '🔴 Inactive'}</p>
{/* Style object */}
<div
style={{
padding: '20px',
backgroundColor: user.isActive ? '#e8f5e9' : '#ffebee',
borderRadius: '8px',
}}
>
Profile card
</div>
</div>
);
}
// 🎯 NHIỆM VỤ CỦA BẠN:
/**
* TODO 1: Tạo component "ProductCard"
* Display:
* - Product name
* - Price (formatted as $XX.XX)
* - Discount percentage (if any)
* - Final price after discount
* - Stock status (In Stock / Out of Stock)
*/
function ProductCard() {
const product = {
name: 'Wireless Headphones',
price: 99.99,
discount: 0.15, // 15%
inStock: true,
};
// YOUR CODE HERE
// Calculate:
// - discountAmount
// - finalPrice
// - Display with proper formatting
}
/**
* TODO 2: Tạo component "WeatherWidget"
* Display:
* - City name
* - Temperature (in Celsius and Fahrenheit)
* - Weather condition with emoji
* - Last updated time
*/
function WeatherWidget() {
const weather = {
city: 'San Francisco',
tempCelsius: 20,
condition: 'sunny', // sunny, cloudy, rainy
lastUpdated: new Date(),
};
// YOUR CODE HERE
// Calculate:
// - tempFahrenheit = (tempCelsius * 9/5) + 32
// - weatherEmoji based on condition
// - formatted time
}
/**
* TODO 3: Tạo component "BlogPost"
* Display:
* - Title
* - Author name
* - Published date (formatted)
* - Reading time
* - Tags list
* - Content preview (first 100 characters)
*/
function BlogPost() {
const post = {
title: 'Getting Started with React',
author: 'Jane Smith',
publishedDate: new Date('2024-01-15'),
readingTime: 5, // minutes
tags: ['React', 'JavaScript', 'Tutorial'],
content:
'React is a powerful JavaScript library for building user interfaces. In this tutorial, we will explore the fundamentals of React and learn how to create your first component. React makes it easy to create interactive UIs by breaking them down into reusable components.',
};
// YOUR CODE HERE
// Calculate/Format:
// - Format date as 'Jan 15, 2024'
// - Content preview (first 100 chars + '...')
// - Display tags as comma-separated list
// - Show reading time with proper styling
}
// 📝 Expected Output:
// ProductCard: Should show price, discount, final price with $ symbol
// WeatherWidget: Should show both °C and °F, emoji for condition
// BlogPost: Should show formatted date, tags, content preview💡 Solution
import React from 'react';
/**
* ✅ SOLUTION: Exercise 1 - JSX Basics
*/
// TODO 1: ProductCard
function ProductCard() {
const product = {
name: 'Wireless Headphones',
price: 99.99,
discount: 0.15, // 15%
inStock: true,
};
// Calculations
const discountAmount = product.price * product.discount;
const finalPrice = product.price - discountAmount;
const discountPercent = product.discount * 100;
return (
<div
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '20px',
margin: '20px',
maxWidth: '300px',
}}
>
<h2 style={{ margin: '0 0 10px 0' }}>{product.name}</h2>
<p
style={{
textDecoration: product.discount > 0 ? 'line-through' : 'none',
color: '#888',
}}
>
Original: ${product.price.toFixed(2)}
</p>
{product.discount > 0 && (
<p style={{ color: '#e74c3c', fontWeight: 'bold' }}>
{discountPercent}% OFF - Save ${discountAmount.toFixed(2)}
</p>
)}
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#27ae60' }}>
${finalPrice.toFixed(2)}
</p>
<p
style={{
padding: '8px 16px',
backgroundColor: product.inStock ? '#d4edda' : '#f8d7da',
color: product.inStock ? '#155724' : '#721c24',
borderRadius: '4px',
textAlign: 'center',
fontWeight: 'bold',
}}
>
{product.inStock ? '✅ In Stock' : '❌ Out of Stock'}
</p>
</div>
);
}
// TODO 2: WeatherWidget
function WeatherWidget() {
const weather = {
city: 'San Francisco',
tempCelsius: 20,
condition: 'sunny', // sunny, cloudy, rainy
lastUpdated: new Date(),
};
// Calculations
const tempFahrenheit = Math.round((weather.tempCelsius * 9) / 5 + 32);
// Weather emoji mapping
const weatherEmojis = {
sunny: '☀️',
cloudy: '☁️',
rainy: '🌧️',
};
const weatherEmoji = weatherEmojis[weather.condition] || '🌤️';
// Format time
const formattedTime = weather.lastUpdated.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
});
return (
<div
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
borderRadius: '16px',
padding: '30px',
margin: '20px',
maxWidth: '300px',
textAlign: 'center',
}}
>
<h2 style={{ margin: '0 0 20px 0', fontSize: '28px' }}>{weather.city}</h2>
<div style={{ fontSize: '80px', margin: '20px 0' }}>{weatherEmoji}</div>
<div style={{ fontSize: '48px', fontWeight: 'bold', margin: '20px 0' }}>
{weather.tempCelsius}°C
</div>
<p style={{ fontSize: '18px', opacity: 0.9 }}>({tempFahrenheit}°F)</p>
<p
style={{
textTransform: 'capitalize',
fontSize: '20px',
margin: '15px 0',
}}
>
{weather.condition}
</p>
<p style={{ fontSize: '14px', opacity: 0.8, marginTop: '20px' }}>
Last updated: {formattedTime}
</p>
</div>
);
}
// TODO 3: BlogPost
function BlogPost() {
const post = {
title: 'Getting Started with React',
author: 'Jane Smith',
publishedDate: new Date('2024-01-15'),
readingTime: 5, // minutes
tags: ['React', 'JavaScript', 'Tutorial'],
content:
'React is a powerful JavaScript library for building user interfaces. In this tutorial, we will explore the fundamentals of React and learn how to create your first component. React makes it easy to create interactive UIs by breaking them down into reusable components.',
};
// Format date
const formattedDate = post.publishedDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
// Content preview
const contentPreview =
post.content.length > 100
? post.content.substring(0, 100) + '...'
: post.content;
return (
<article
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
padding: '24px',
margin: '20px',
maxWidth: '600px',
backgroundColor: '#fff',
}}
>
<h1
style={{
margin: '0 0 12px 0',
fontSize: '28px',
color: '#1a1a1a',
}}
>
{post.title}
</h1>
<div
style={{
display: 'flex',
gap: '16px',
marginBottom: '16px',
color: '#666',
fontSize: '14px',
}}
>
<span>By {post.author}</span>
<span>•</span>
<span>{formattedDate}</span>
<span>•</span>
<span>{post.readingTime} min read</span>
</div>
<div style={{ marginBottom: '16px' }}>
{post.tags.map((tag, index) => (
<span
key={index}
style={{
display: 'inline-block',
backgroundColor: '#e3f2fd',
color: '#1976d2',
padding: '4px 12px',
borderRadius: '16px',
marginRight: '8px',
fontSize: '12px',
fontWeight: '500',
}}
>
#{tag}
</span>
))}
</div>
<p
style={{
lineHeight: '1.6',
color: '#444',
fontSize: '16px',
}}
>
{contentPreview}
</p>
<a
href='#'
style={{
color: '#1976d2',
textDecoration: 'none',
fontWeight: '500',
fontSize: '14px',
}}
>
Read more →
</a>
</article>
);
}
// Main App Component
export default function App() {
return (
<div
style={{
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
backgroundColor: '#f5f5f5',
minHeight: '100vh',
padding: '20px',
}}
>
<h1 style={{ textAlign: 'center', color: '#333' }}>
Exercise 1: JSX Basics Solutions
</h1>
<ProductCard />
<WeatherWidget />
<BlogPost />
</div>
);
}🎓 Kiến thức trọng tâm:
- Sử dụng biểu thức JSX với dấu
{ } - Inline style dưới dạng object JavaScript
- Render có điều kiện bằng toán tử ternary
- Dùng template literals để định dạng chuỗi
- Render danh sách bằng
Array.map() - Tính toán và biến đổi dữ liệu trước khi render
⭐⭐ Exercise 2: JSX vs HTML Conversion (25 phút)
/**
* 🎯 Mục tiêu: Chuyển đổi HTML sang JSX đúng cách
* ⏱️ Thời gian: 25 phút
*
* Scenario: Bạn có HTML template cần convert sang React component
*
* 🤔 PHÂN TÍCH:
* Approach A: Copy-paste HTML trực tiếp vào JSX
* Pros: Nhanh
* Cons: Sẽ có lỗi syntax (class, for, style...)
*
* Approach B: Convert từng phần, fix syntax issues
* Pros: Hiểu rõ differences, code chạy đúng
* Cons: Mất thời gian hơn
*
* 💭 CHỌN APPROACH B - Learn by doing
*/
// ========================================
// HTML TEMPLATE - CẦN CONVERT
// ========================================
const htmlTemplate = `
<div class="login-form">
<form onsubmit="handleSubmit(event)">
<h2 class="form-title">Login</h2>
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
placeholder="Enter your email"
required
>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
placeholder="Enter your password"
required
>
</div>
<div class="form-group">
<input type="checkbox" id="remember" name="remember">
<label for="remember">Remember me</label>
</div>
<button type="submit" class="btn-primary">
Login
</button>
<p class="form-footer">
Don't have an account?
<a href="/signup" class="link">Sign up</a>
</p>
</form>
</div>
`;
// TODO 1: Convert HTML trên sang JSX component
// Identify và fix tất cả JSX syntax issues
function LoginForm() {
// YOUR CODE HERE
// Issues cần fix:
// - class → className
// - for → htmlFor
// - onsubmit → onSubmit
// - Self-closing tags
// - Event handler format
// - Any other JSX rules
}
// ========================================
// HTML TEMPLATE 2 - WITH INLINE STYLES
// ========================================
const htmlWithStyles = `
<div class="card" style="padding: 20px; background-color: white; border-radius: 8px;">
<img src="avatar.jpg" alt="User avatar" style="width: 100px; height: 100px; border-radius: 50%;">
<h3 style="margin-top: 10px; font-size: 24px; color: #333;">John Doe</h3>
<p style="color: #666; font-size: 14px;">Software Developer</p>
<div style="margin-top: 20px; display: flex; gap: 10px;">
<button onclick="followUser()" style="padding: 8px 16px; background-color: #3b82f6; color: white; border: none; border-radius: 4px;">
Follow
</button>
<button onclick="messageUser()" style="padding: 8px 16px; background-color: white; color: #3b82f6; border: 1px solid #3b82f6; border-radius: 4px;">
Message
</button>
</div>
</div>
`;
// TODO 2: Convert HTML with inline styles sang JSX
// Convert style attribute thành style object
function UserCard() {
// YOUR CODE HERE
// Fix:
// - style string → style object
// - CSS properties to camelCase (background-color → backgroundColor)
// - onclick → onClick
// - Other JSX issues
}
// ========================================
// HTML TEMPLATE 3 - COMPLEX STRUCTURE
// ========================================
const complexHTML = `
<nav class="navbar">
<div class="nav-brand">
<img src="logo.png" alt="Logo">
<span>MyApp</span>
</div>
<ul class="nav-menu">
<li class="nav-item active">
<a href="/" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/about" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/contact" class="nav-link">Contact</a>
</li>
</ul>
<div class="nav-actions">
<input type="search" placeholder="Search..." class="search-input">
<button class="btn-search">Search</button>
</div>
</nav>
`;
// TODO 3: Convert complex navigation HTML sang JSX
function Navbar() {
// YOUR CODE HERE
// Consider:
// - Multiple className fixes
// - Self-closing tags
// - Structure preservation
// - Semantic meaning
}
// 📝 VALIDATION CHECKLIST:
// - [ ] All 'class' converted to 'className'
// - [ ] All 'for' converted to 'htmlFor'
// - [ ] All inline styles are objects with camelCase
// - [ ] All self-closing tags have />
// - [ ] All event handlers are camelCase (onClick, onSubmit)
// - [ ] No syntax errors when rendered💡 Solution
import React from 'react';
/**
* ✅ SOLUTION: Exercise 2 - HTML to JSX Conversion
*/
// TODO 1: LoginForm
function LoginForm() {
const handleSubmit = (event) => {
event.preventDefault();
alert('Form submitted! (This is a demo)');
};
return (
<div
style={{
maxWidth: '400px',
padding: '32px',
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
margin: '20px',
}}
>
<div>
<h2
style={{
marginTop: 0,
marginBottom: '24px',
textAlign: 'center',
color: '#1f2937',
}}
>
Login
</h2>
<div style={{ marginBottom: '16px' }}>
<label
htmlFor='email'
style={{
display: 'block',
marginBottom: '8px',
fontWeight: '500',
color: '#374151',
}}
>
Email Address
</label>
<input
type='email'
id='email'
name='email'
placeholder='Enter your email'
required
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '14px',
boxSizing: 'border-box',
}}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label
htmlFor='password'
style={{
display: 'block',
marginBottom: '8px',
fontWeight: '500',
color: '#374151',
}}
>
Password
</label>
<input
type='password'
id='password'
name='password'
placeholder='Enter your password'
required
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '14px',
boxSizing: 'border-box',
}}
/>
</div>
<div
style={{
marginBottom: '20px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<input
type='checkbox'
id='remember'
name='remember'
/>
<label
htmlFor='remember'
style={{ fontSize: '14px', color: '#6b7280' }}
>
Remember me
</label>
</div>
<button
type='button'
onClick={handleSubmit}
style={{
width: '100%',
padding: '12px',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '16px',
fontWeight: '500',
cursor: 'pointer',
}}
>
Login
</button>
<p
style={{
marginTop: '20px',
textAlign: 'center',
fontSize: '14px',
color: '#6b7280',
}}
>
Don't have an account?{' '}
<a
href='#signup'
style={{
color: '#3b82f6',
textDecoration: 'none',
fontWeight: '500',
}}
>
Sign up
</a>
</p>
</div>
</div>
);
}
// TODO 2: UserCard
function UserCard() {
const followUser = () => {
alert('Following user...');
};
const messageUser = () => {
alert('Opening message...');
};
return (
<div
style={{
padding: '20px',
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
textAlign: 'center',
maxWidth: '300px',
margin: '20px',
}}
>
<img
src='https://i.pravatar.cc/100'
alt='User avatar'
style={{
width: '100px',
height: '100px',
borderRadius: '50%',
objectFit: 'cover',
}}
/>
<h3
style={{
marginTop: '10px',
fontSize: '24px',
color: '#333',
}}
>
John Doe
</h3>
<p
style={{
color: '#666',
fontSize: '14px',
}}
>
Software Developer
</p>
<div
style={{
marginTop: '20px',
display: 'flex',
gap: '10px',
justifyContent: 'center',
}}
>
<button
onClick={followUser}
style={{
padding: '8px 16px',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: '500',
}}
>
Follow
</button>
<button
onClick={messageUser}
style={{
padding: '8px 16px',
backgroundColor: 'white',
color: '#3b82f6',
border: '1px solid #3b82f6',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: '500',
}}
>
Message
</button>
</div>
</div>
);
}
// TODO 3: Navbar
function Navbar() {
const navItems = [
{ label: 'Home', href: '#home', active: true },
{ label: 'About', href: '#about', active: false },
{ label: 'Contact', href: '#contact', active: false },
];
const handleSearch = () => {
alert('Searching...');
};
return (
<nav
style={{
backgroundColor: '#1f2937',
padding: '16px 32px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
color: 'white',
flexWrap: 'wrap',
gap: '16px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<img
src='https://via.placeholder.com/40'
alt='Logo'
style={{ width: '40px', height: '40px', borderRadius: '4px' }}
/>
<span style={{ fontSize: '20px', fontWeight: 'bold' }}>MyApp</span>
</div>
<ul
style={{
display: 'flex',
listStyle: 'none',
gap: '24px',
margin: 0,
padding: 0,
}}
>
{navItems.map((item, index) => (
<li key={index}>
<a
href={item.href}
style={{
color: item.active ? '#60a5fa' : 'white',
textDecoration: 'none',
fontWeight: item.active ? 'bold' : 'normal',
padding: '8px 12px',
borderRadius: '4px',
backgroundColor: item.active
? 'rgba(96, 165, 250, 0.1)'
: 'transparent',
}}
>
{item.label}
</a>
</li>
))}
</ul>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type='search'
placeholder='Search...'
style={{
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #4b5563',
backgroundColor: '#374151',
color: 'white',
outline: 'none',
}}
/>
<button
onClick={handleSearch}
style={{
padding: '8px 16px',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: '500',
}}
>
Search
</button>
</div>
</nav>
);
}
// Main App
export default function App() {
return (
<div
style={{
backgroundColor: '#f3f4f6',
minHeight: '100vh',
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}}
>
<h1
style={{
textAlign: 'center',
padding: '20px',
color: '#1f2937',
margin: 0,
}}
>
Exercise 2: HTML to JSX Conversion
</h1>
<Navbar />
<div
style={{
display: 'flex',
gap: '20px',
flexWrap: 'wrap',
justifyContent: 'center',
padding: '40px 20px',
}}
>
<LoginForm />
<UserCard />
</div>
</div>
);
}**🔧 Conversion Checklist:**
- `class` → `className`
- `for` → `htmlFor`
- `onsubmit` → `onSubmit`
- `onclick` → `onClick`
- `style="string"` → `style={{ ... }}`
- `background-color` → `backgroundColor`
- Thẻ self-closing bắt buộc có `/`
- Event handler phải là function, không phải string🎓 Kiến thức rút ra:
- Thuộc tính trong JSX dùng camelCase
- Style viết dưới dạng object JavaScript
- Event handler truyền vào dưới dạng function
- Mọi thẻ JSX đều phải được đóng đúng cách
- Các từ khóa HTML bị trùng phải dùng tên thay thế trong JSX
⭐⭐⭐ Exercise 3: Dynamic UI with JSX (40 phút)
/**
* 🎯 Mục tiêu: Xây dựng UI động với JSX expressions
* ⏱️ Thời gian: 40 phút
*
* 📋 Product Requirements:
* User Story: "Là user, tôi muốn thấy danh sách products với
* filtering và sorting, để dễ dàng tìm product mình muốn"
*
* ✅ Acceptance Criteria:
* - [ ] Hiển thị list of products
* - [ ] Show/hide out of stock products
* - [ ] Filter by category
* - [ ] Sort by price/name
* - [ ] Display product count
* - [ ] Conditional styling based on stock status
*
* 🎨 Technical Constraints:
* - CHỈ dùng: JSX, variables, expressions (Ngày 1-3)
* - KHÔNG dùng: State, Events with updates, useEffect
*
* 🚨 Edge Cases cần handle:
* - Empty product list
* - All products out of stock
* - No products match filter
*/
// Sample data
const products = [
{
id: 1,
name: 'Laptop Pro',
price: 1299,
category: 'Electronics',
inStock: true,
rating: 4.5,
},
{
id: 2,
name: 'Wireless Mouse',
price: 29,
category: 'Electronics',
inStock: true,
rating: 4.2,
},
{
id: 3,
name: 'Mechanical Keyboard',
price: 89,
category: 'Electronics',
inStock: false,
rating: 4.7,
},
{
id: 4,
name: 'USB-C Cable',
price: 15,
category: 'Accessories',
inStock: true,
rating: 4.0,
},
{
id: 5,
name: 'Monitor Stand',
price: 45,
category: 'Accessories',
inStock: true,
rating: 4.3,
},
{
id: 6,
name: '4K Monitor',
price: 399,
category: 'Electronics',
inStock: false,
rating: 4.8,
},
{
id: 7,
name: 'Desk Lamp',
price: 35,
category: 'Furniture',
inStock: true,
rating: 4.1,
},
{
id: 8,
name: 'Webcam HD',
price: 79,
category: 'Electronics',
inStock: true,
rating: 4.4,
},
];
// TODO 1: Tạo ProductList component
function ProductList() {
// Config (hardcode for now - sẽ dùng state sau)
const showOutOfStock = true;
const selectedCategory = 'All'; // 'All', 'Electronics', 'Accessories', 'Furniture'
const sortBy = 'name'; // 'name', 'price', 'rating'
// YOUR CODE HERE
// Tasks:
// 1. Filter products based on showOutOfStock và selectedCategory
// 2. Sort products based on sortBy
// 3. Display filtered & sorted products
// 4. Show product count
// 5. Handle empty results
return <div>{/* YOUR JSX HERE */}</div>;
}
// TODO 2: Tạo ProductCard component (single product)
function ProductCard({ product }) {
// YOUR CODE HERE
// Display:
// - Product name
// - Price (formatted)
// - Category badge
// - Rating stars
// - Stock status (with conditional styling)
// - Different opacity/style if out of stock
return <div>{/* YOUR JSX HERE */}</div>;
}
// TODO 3: Tạo CategoryBadge component
function CategoryBadge({ category }) {
// YOUR CODE HERE
// Different color for each category:
// - Electronics: blue
// - Accessories: green
// - Furniture: orange
return <span>{/* YOUR JSX HERE */}</span>;
}
// TODO 4: Tạo RatingStars component
function RatingStars({ rating }) {
// YOUR CODE HERE
// Display stars based on rating
// - Full stars for whole numbers
// - Half star for decimals
// - Empty stars for remainder
// Example: 4.5 → ★★★★☆
return <div>{/* YOUR JSX HERE */}</div>;
}
// TODO 5: Tạo EmptyState component
function EmptyState({ message }) {
// YOUR CODE HERE
// Display when no products match filters
// Show icon + message + suggestion
return <div>{/* YOUR JSX HERE */}</div>;
}
// Main App
function App() {
return (
<div>
<h1>Product Catalog</h1>
<ProductList />
</div>
);
}
// 📝 Testing Scenarios:
// 1. All products visible
// 2. Hide out of stock → should show 6 products
// 3. Filter by Electronics → should show 5 products
// 4. Filter Electronics + hide out of stock → should show 3 products
// 5. Sort by price ascending
// 6. Sort by rating descending
// 7. No products match filter → show empty state💡 Solution
import React from 'react';
/**
* ✅ SOLUTION: Exercise 3 - Dynamic UI with JSX
*/
// Sample data
const products = [
{
id: 1,
name: 'Laptop Pro',
price: 1299,
category: 'Electronics',
inStock: true,
rating: 4.5,
},
{
id: 2,
name: 'Wireless Mouse',
price: 29,
category: 'Electronics',
inStock: true,
rating: 4.2,
},
{
id: 3,
name: 'Mechanical Keyboard',
price: 89,
category: 'Electronics',
inStock: false,
rating: 4.7,
},
{
id: 4,
name: 'USB-C Cable',
price: 15,
category: 'Accessories',
inStock: true,
rating: 4.0,
},
{
id: 5,
name: 'Monitor Stand',
price: 45,
category: 'Accessories',
inStock: true,
rating: 4.3,
},
{
id: 6,
name: '4K Monitor',
price: 399,
category: 'Electronics',
inStock: false,
rating: 4.8,
},
{
id: 7,
name: 'Desk Lamp',
price: 35,
category: 'Furniture',
inStock: true,
rating: 4.1,
},
{
id: 8,
name: 'Webcam HD',
price: 79,
category: 'Electronics',
inStock: true,
rating: 4.4,
},
];
// TODO 4: RatingStars component
function RatingStars({ rating }) {
const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 >= 0.5;
const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
return (
<div
style={{
color: '#fbbf24',
fontSize: '16px',
display: 'flex',
gap: '2px',
}}
>
{/* Full stars */}
{Array(fullStars)
.fill(0)
.map((_, i) => (
<span key={`full-${i}`}>★</span>
))}
{/* Half star */}
{hasHalfStar && <span>⯨</span>}
{/* Empty stars */}
{Array(emptyStars)
.fill(0)
.map((_, i) => (
<span
key={`empty-${i}`}
style={{ color: '#d1d5db' }}
>
★
</span>
))}
<span style={{ marginLeft: '6px', fontSize: '14px', color: '#6b7280' }}>
{rating.toFixed(1)}
</span>
</div>
);
}
// TODO 3: CategoryBadge component
function CategoryBadge({ category }) {
const colors = {
Electronics: { bg: '#dbeafe', text: '#1e40af' },
Accessories: { bg: '#d1fae5', text: '#065f46' },
Furniture: { bg: '#fed7aa', text: '#9a3412' },
};
const color = colors[category] || { bg: '#f3f4f6', text: '#374151' };
return (
<span
style={{
display: 'inline-block',
padding: '4px 12px',
backgroundColor: color.bg,
color: color.text,
borderRadius: '12px',
fontSize: '12px',
fontWeight: '600',
}}
>
{category}
</span>
);
}
// TODO 2: ProductCard component
function ProductCard({ product }) {
return (
<div
style={{
backgroundColor: 'white',
borderRadius: '8px',
padding: '20px',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
opacity: product.inStock ? 1 : 0.6,
border: product.inStock ? '1px solid #e5e7eb' : '1px solid #fca5a5',
transition: 'transform 0.2s',
cursor: 'pointer',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '12px',
}}
>
<h3
style={{
margin: 0,
fontSize: '18px',
color: product.inStock ? '#1f2937' : '#9ca3af',
}}
>
{product.name}
</h3>
<CategoryBadge category={product.category} />
</div>
<div style={{ marginBottom: '12px' }}>
<RatingStars rating={product.rating} />
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div
style={{
fontSize: '24px',
fontWeight: 'bold',
color: product.inStock ? '#059669' : '#9ca3af',
}}
>
${product.price.toLocaleString()}
</div>
<div
style={{
padding: '6px 12px',
backgroundColor: product.inStock ? '#d1fae5' : '#fee2e2',
color: product.inStock ? '#065f46' : '#991b1b',
borderRadius: '6px',
fontSize: '13px',
fontWeight: '600',
}}
>
{product.inStock ? '✓ In Stock' : '✕ Out of Stock'}
</div>
</div>
</div>
);
}
// TODO 5: EmptyState component
function EmptyState({ message }) {
return (
<div
style={{
textAlign: 'center',
padding: '60px 20px',
backgroundColor: '#f9fafb',
borderRadius: '8px',
border: '2px dashed #d1d5db',
}}
>
<div style={{ fontSize: '64px', marginBottom: '16px' }}>📦</div>
<h3
style={{
fontSize: '20px',
color: '#6b7280',
margin: '0 0 8px 0',
}}
>
{message}
</h3>
<p style={{ color: '#9ca3af', margin: 0 }}>
Try adjusting your filters or check back later
</p>
</div>
);
}
// TODO 1: ProductList component
function ProductList() {
// Configuration (hardcoded for now - will use state later)
const showOutOfStock = true;
const selectedCategory = 'All'; // Try: 'Electronics', 'Accessories', 'Furniture'
const sortBy = 'name'; // Try: 'price', 'rating'
// Step 1: Filter by stock status
let filteredProducts = showOutOfStock
? products
: products.filter((p) => p.inStock);
// Step 2: Filter by category
if (selectedCategory !== 'All') {
filteredProducts = filteredProducts.filter(
(p) => p.category === selectedCategory
);
}
// Step 3: Sort products
const sortedProducts = [...filteredProducts].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
} else if (sortBy === 'price') {
return a.price - b.price;
} else if (sortBy === 'rating') {
return b.rating - a.rating; // Descending
}
return 0;
});
// Calculate stats
const totalProducts = products.length;
const displayedProducts = sortedProducts.length;
const inStockCount = sortedProducts.filter((p) => p.inStock).length;
const outOfStockCount = sortedProducts.filter((p) => !p.inStock).length;
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
{/* Header with stats */}
<div
style={{
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
marginBottom: '24px',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
}}
>
<h2 style={{ margin: '0 0 16px 0', color: '#1f2937' }}>
Product Catalog
</h2>
<div
style={{
display: 'flex',
gap: '24px',
flexWrap: 'wrap',
}}
>
<div>
<div
style={{
fontSize: '12px',
color: '#6b7280',
marginBottom: '4px',
}}
>
Total Products
</div>
<div
style={{ fontSize: '24px', fontWeight: 'bold', color: '#1f2937' }}
>
{totalProducts}
</div>
</div>
<div>
<div
style={{
fontSize: '12px',
color: '#6b7280',
marginBottom: '4px',
}}
>
Showing
</div>
<div
style={{ fontSize: '24px', fontWeight: 'bold', color: '#3b82f6' }}
>
{displayedProducts}
</div>
</div>
<div>
<div
style={{
fontSize: '12px',
color: '#6b7280',
marginBottom: '4px',
}}
>
In Stock
</div>
<div
style={{ fontSize: '24px', fontWeight: 'bold', color: '#059669' }}
>
{inStockCount}
</div>
</div>
<div>
<div
style={{
fontSize: '12px',
color: '#6b7280',
marginBottom: '4px',
}}
>
Out of Stock
</div>
<div
style={{ fontSize: '24px', fontWeight: 'bold', color: '#dc2626' }}
>
{outOfStockCount}
</div>
</div>
</div>
{/* Current filters display */}
<div
style={{
marginTop: '16px',
padding: '12px',
backgroundColor: '#f9fafb',
borderRadius: '6px',
fontSize: '14px',
color: '#6b7280',
}}
>
<strong>Active Filters:</strong> Category:{' '}
<span style={{ color: '#3b82f6', fontWeight: '600' }}>
{selectedCategory}
</span>{' '}
| Show Out of Stock:{' '}
<span style={{ color: '#3b82f6', fontWeight: '600' }}>
{showOutOfStock ? 'Yes' : 'No'}
</span>{' '}
| Sort by:{' '}
<span style={{ color: '#3b82f6', fontWeight: '600' }}>{sortBy}</span>
</div>
</div>
{/* Product grid */}
{sortedProducts.length > 0 ? (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: '20px',
}}
>
{sortedProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
) : (
<EmptyState message='No products found' />
)}
</div>
);
}
// Main App
export default function App() {
return (
<div
style={{
backgroundColor: '#f3f4f6',
minHeight: '100vh',
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}}
>
<ProductList />
</div>
);
}**🎓 Những kiến thức đã học:**
- **Lọc mảng (Filtering arrays):** Sử dụng `.filter()` để hiển thị / ẩn sản phẩm theo điều kiện
- **Sắp xếp mảng (Sorting arrays):** Dùng `.sort()` với nhiều tiêu chí khác nhau
- **Render có điều kiện:** Thay đổi UI dựa trên dữ liệu
- **Style có điều kiện:** Màu sắc / độ mờ thay đổi theo trạng thái
- **Component composition:** Chia UI thành các component nhỏ, tái sử dụng
- **Render danh sách:** Dùng `.map()` để hiển thị list
- **Tính toán trước khi render:** Xử lý logic trước, JSX chỉ để hiển thị
**💡 Thử thay đổi để hiểu sâu hơn:**
- Đặt `showOutOfStock = false` để ẩn sản phẩm hết hàng
- Thay đổi `selectedCategory` thành `Electronics` hoặc `Accessories`
- Đổi `sortBy` sang `price` hoặc `rating`
- Chỉnh lại màu sắc trong component `CategoryBadge`⭐⭐⭐⭐ Exercise 4: Component Composition Architecture (60 phút)
/**
* 🎯 Mục tiêu: Thiết kế component architecture hợp lý
* ⏱️ Thời gian: 60 phút
*
* 🏗️ PHASE 1: Research & Design (20 phút)
*
* Scenario: Bạn cần build Dashboard Layout với:
* - Header (logo, navigation, user menu)
* - Sidebar (navigation links)
* - Main content area
* - Footer
*
* 📋 ADR (Architecture Decision Record):
*
* ## Context
* Cần component structure dễ maintain và reusable
*
* ## Decision Points
*
* 1. Nên chia components như thế nào?
* - Option A: Tất cả trong 1 component
* - Option B: Chia theo UI sections (Header, Sidebar, Main, Footer)
* - Option C: Chia theo functionality (Navigation, Content, UserInfo)
*
* 2. Nên đặt tên components thế nào?
* - Option A: Generic (Box, Container, Section)
* - Option B: Specific (DashboardHeader, DashboardSidebar)
* - Option C: Mix (Layout.Header, Layout.Sidebar)
*
* 3. Component nesting depth?
* - Option A: Flat structure (ít nesting)
* - Option B: Deep nesting (hierarchical)
* - Option C: Balanced (2-3 levels)
*/
// YOUR ANALYSIS HERE
// Document your decisions với rationale
// ========================================
// 💻 PHASE 2: Implementation (30 phút)
// ========================================
/**
* TODO: Implement Dashboard Layout
* Requirements:
* - Reusable components
* - Clear component hierarchy
* - Semantic naming
* - Props for customization (CHỈ pass data, KHÔNG có callbacks)
*/
// Component Structure Example:
// App
// ├─ DashboardLayout
// │ ├─ Header
// │ │ ├─ Logo
// │ │ ├─ Navigation
// │ │ └─ UserMenu
// │ ├─ Sidebar
// │ │ └─ NavLinks
// │ ├─ MainContent
// │ └─ Footer
// TODO 1: Tạo Logo component
function Logo({ appName, size }) {
// YOUR CODE HERE
// Display logo với app name
// Size variants: 'small', 'medium', 'large'
}
// TODO 2: Tạo Navigation component
function Navigation({ links }) {
// YOUR CODE HERE
// links = [{ label: 'Home', href: '/', active: true }, ...]
// Hiển thị nav links với active state
}
// TODO 3: Tạo UserMenu component
function UserMenu({ user }) {
// YOUR CODE HERE
// user = { name: 'John', avatar: 'url', role: 'Admin' }
// Display user info
}
// TODO 4: Tạo Header component (compose Logo + Navigation + UserMenu)
function Header({ appName, navLinks, user }) {
// YOUR CODE HERE
// Compose các components nhỏ hơn
}
// TODO 5: Tạo Sidebar component
function Sidebar({ links }) {
// YOUR CODE HERE
// Vertical navigation
// Group links by category nếu có
}
// TODO 6: Tạo MainContent component
function MainContent({ title, children }) {
// YOUR CODE HERE
// Wrapper cho main content area
// children = actual page content
}
// TODO 7: Tạo Footer component
function Footer({ companyName, year, links }) {
// YOUR CODE HERE
// Copyright info + footer links
}
// TODO 8: Tạo DashboardLayout component (compose all)
function DashboardLayout({
appName,
headerLinks,
sidebarLinks,
user,
children,
}) {
// YOUR CODE HERE
// Compose: Header + Sidebar + MainContent + Footer
// Use CSS Grid or Flexbox for layout
}
// ========================================
// 🧪 PHASE 3: Testing (10 phút)
// ========================================
function App() {
// Sample data
const appName = 'My Dashboard';
const headerLinks = [
{ label: 'Dashboard', href: '/', active: true },
{ label: 'Analytics', href: '/analytics', active: false },
{ label: 'Settings', href: '/settings', active: false },
];
const sidebarLinks = [
{
category: 'Main',
items: [
{ label: 'Home', href: '/', icon: '🏠', active: true },
{ label: 'Projects', href: '/projects', icon: '📁', active: false },
],
},
{
category: 'Tools',
items: [
{ label: 'Calendar', href: '/calendar', icon: '📅', active: false },
{ label: 'Tasks', href: '/tasks', icon: '✓', active: false },
],
},
];
const user = {
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://i.pravatar.cc/40',
role: 'Admin',
};
return (
<DashboardLayout
appName={appName}
headerLinks={headerLinks}
sidebarLinks={sidebarLinks}
user={user}
>
{/* Main page content */}
<div>
<h1>Welcome to Dashboard</h1>
<p>This is the main content area.</p>
</div>
</DashboardLayout>
);
}
// 📝 Evaluation Criteria:
// - [ ] Clear component hierarchy
// - [ ] Reusable components
// - [ ] Semantic naming
// - [ ] Props properly structured
// - [ ] No hardcoded values
// - [ ] Layout works responsively
// - [ ] Code is readable💡 Solution
// Exercise 4: Component Composition Architecture
// Dashboard Layout với cấu trúc component rõ ràng, dễ bảo trì và tái sử dụng
import React from 'react';
// =============================================
// TODO 1: Logo component
// =============================================
function Logo({ appName, size = 'medium' }) {
// size variants: small, medium, large
const sizes = {
small: { fontSize: '1.4rem', gap: '6px' },
medium: { fontSize: '1.8rem', gap: '8px' },
large: { fontSize: '2.2rem', gap: '10px' },
};
const style = sizes[size] || sizes.medium;
return (
<div
className='logo'
style={{ display: 'flex', alignItems: 'center', gap: style.gap }}
>
<div
style={{
width: size === 'large' ? 48 : size === 'small' ? 32 : 40,
height: size === 'large' ? 48 : size === 'small' ? 32 : 40,
background: 'linear-gradient(135deg, #667eea, #764ba2)',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontWeight: 'bold',
fontSize:
size === 'large'
? '1.6rem'
: size === 'small'
? '1.1rem'
: '1.4rem',
}}
>
{appName.charAt(0)}
</div>
<span
style={{ fontSize: style.fontSize, fontWeight: 700, color: '#1f2937' }}
>
{appName}
</span>
</div>
);
}
// =============================================
// TODO 2: Navigation component (dùng cho header)
// =============================================
function Navigation({ links }) {
return (
<nav className='main-navigation'>
<ul
style={{
display: 'flex',
listStyle: 'none',
gap: '32px',
margin: 0,
padding: 0,
}}
>
{links.map((link) => (
<li key={link.href}>
<a
href={link.href}
style={{
color: link.active ? '#3b82f6' : '#4b5563',
textDecoration: 'none',
fontWeight: link.active ? 600 : 500,
padding: '8px 12px',
borderRadius: '6px',
background: link.active
? 'rgba(59, 130, 246, 0.1)'
: 'transparent',
}}
>
{link.label}
</a>
</li>
))}
</ul>
</nav>
);
}
// =============================================
// TODO 3: UserMenu component
// =============================================
function UserMenu({ user }) {
return (
<div
className='user-menu'
style={{ display: 'flex', alignItems: 'center', gap: '12px' }}
>
<img
src={user.avatar}
alt={`${user.name} avatar`}
style={{
width: 40,
height: 40,
borderRadius: '50%',
objectFit: 'cover',
border: '2px solid #e5e7eb',
}}
/>
<div>
<div style={{ fontWeight: 600, color: '#1f2937' }}>{user.name}</div>
<div style={{ fontSize: '0.85rem', color: '#6b7280' }}>{user.role}</div>
</div>
</div>
);
}
// =============================================
// TODO 4: Header component (compose Logo + Navigation + UserMenu)
// =============================================
function Header({ appName, navLinks, user }) {
return (
<header
style={{
background: 'white',
borderBottom: '1px solid #e5e7eb',
padding: '16px 32px',
position: 'sticky',
top: 0,
zIndex: 100,
boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
maxWidth: '1400px',
margin: '0 auto',
}}
>
<Logo
appName={appName}
size='medium'
/>
<Navigation links={navLinks} />
<UserMenu user={user} />
</div>
</header>
);
}
// =============================================
// TODO 5: Sidebar component (có nhóm category)
// =============================================
function Sidebar({ links }) {
return (
<aside
style={{
width: 260,
background: '#f8f9fa',
borderRight: '1px solid #e5e7eb',
padding: '24px 16px',
height: 'calc(100vh - 64px)', // trừ header height
position: 'fixed',
overflowY: 'auto',
}}
>
{links.map((group) => (
<div
key={group.category}
style={{ marginBottom: '24px' }}
>
<h4
style={{
color: '#6b7280',
fontSize: '0.85rem',
fontWeight: 600,
margin: '0 0 12px 12px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
{group.category}
</h4>
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{group.items.map((item) => (
<li key={item.href}>
<a
href={item.href}
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '12px 16px',
color: item.active ? '#3b82f6' : '#4b5563',
textDecoration: 'none',
borderRadius: '8px',
background: item.active
? 'rgba(59, 130, 246, 0.1)'
: 'transparent',
fontWeight: item.active ? 600 : 500,
transition: 'all 0.15s',
}}
>
<span style={{ fontSize: '1.2rem' }}>{item.icon}</span>
{item.label}
</a>
</li>
))}
</ul>
</div>
))}
</aside>
);
}
// =============================================
// TODO 6: MainContent component
// =============================================
function MainContent({ title, children }) {
return (
<main
style={{
marginLeft: 260, // bằng width của sidebar
padding: '32px 40px',
minHeight: 'calc(100vh - 64px)',
}}
>
{title && (
<h1
style={{
margin: '0 0 32px',
fontSize: '2rem',
color: '#1f2937',
}}
>
{title}
</h1>
)}
{children}
</main>
);
}
// =============================================
// TODO 7: Footer component
// =============================================
function Footer({ companyName, year, links = [] }) {
return (
<footer
style={{
background: '#1f2937',
color: 'white',
padding: '32px 0',
marginTop: 'auto',
}}
>
<div
style={{
maxWidth: '1400px',
margin: '0 auto',
padding: '0 40px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: '20px',
}}
>
<div>
<strong>{companyName}</strong> © {year}
</div>
<ul
style={{
display: 'flex',
gap: '24px',
listStyle: 'none',
margin: 0,
padding: 0,
}}
>
{links.map((link) => (
<li key={link.href}>
<a
href={link.href}
style={{ color: '#d1d5db', textDecoration: 'none' }}
>
{link.label}
</a>
</li>
))}
</ul>
</div>
</footer>
);
}
// =============================================
// TODO 8: DashboardLayout - component gốc ghép tất cả
// =============================================
function DashboardLayout({
appName,
headerLinks,
sidebarLinks,
user,
children,
title,
}) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
background: '#f3f4f6',
}}
>
<Header
appName={appName}
navLinks={headerLinks}
user={user}
/>
<div style={{ display: 'flex', flex: 1 }}>
<Sidebar links={sidebarLinks} />
<MainContent title={title}>{children}</MainContent>
</div>
<Footer
companyName={appName}
year={new Date().getFullYear()}
links={[
{ label: 'Về chúng tôi', href: '/about' },
{ label: 'Hỗ trợ', href: '/support' },
{ label: 'Điều khoản', href: '/terms' },
]}
/>
</div>
);
}
// =============================================
// App - Testing & Demo
// =============================================
function App() {
const appName = 'My Dashboard';
const headerLinks = [
{ label: 'Dashboard', href: '/', active: true },
{ label: 'Analytics', href: '/analytics', active: false },
{ label: 'Settings', href: '/settings', active: false },
];
const sidebarLinks = [
{
category: 'Main',
items: [
{ label: 'Home', href: '/', icon: '🏠', active: true },
{ label: 'Projects', href: '/projects', icon: '📁', active: false },
],
},
{
category: 'Tools',
items: [
{ label: 'Calendar', href: '/calendar', icon: '📅', active: false },
{ label: 'Tasks', href: '/tasks', icon: '✓', active: false },
],
},
];
const user = {
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://i.pravatar.cc/40',
role: 'Admin',
};
return (
<DashboardLayout
appName={appName}
headerLinks={headerLinks}
sidebarLinks={sidebarLinks}
user={user}
title='Trang tổng quan'
>
<div
style={{
background: 'white',
padding: '32px',
borderRadius: '12px',
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
}}
>
<h1>Chào mừng quay lại, {user.name}!</h1>
<p>Đây là khu vực nội dung chính của dashboard.</p>
<p>
Bạn có thể đặt bất kỳ component nội dung nào vào đây thông qua
children prop.
</p>
</div>
</DashboardLayout>
);
}
export default App;Ghi chú thiết kế (ADR – Architecture Decision Record):
Cách chia component → Chọn Option B + C kết hợp
→ Chia theo UI sections (Header, Sidebar, Main, Footer)
→ Nhưng bên trong Header còn chia nhỏ hơn: Logo + Navigation + UserMenu
→ Lý do: Tăng tính tái sử dụng và dễ test, đồng thời giữ cấu trúc rõ ràngCách đặt tên → Chọn Option B (Specific)
→ DashboardHeader, DashboardSidebar, DashboardFooter…
→ Nhưng trong code dùng tên ngắn gọn: Header, Sidebar, Footer
→ Lý do: Trong ngữ cảnh chỉ có 1 dashboard → không cần tiền tố dài dòngNesting depth → Chọn Option C: Balanced (2-3 levels)
→ App → DashboardLayout → (Header, Sidebar, MainContent, Footer)
→ Header → (Logo, Navigation, UserMenu)
→ Không quá sâu, vẫn dễ theo dõiProps → Chỉ truyền data xuống, không truyền callback (theo yêu cầu)
→ Điều này giúp các component con thuần túy (presentational), dễ testLayout → Sử dụng position: fixed cho Sidebar + margin-left cho Main
→ Responsive: Sidebar cố định bên trái, nội dung scroll độc lập
Nếu muốn nâng cao thêm, có thể:
- Thêm responsive: ẩn Sidebar trên mobile → nút hamburger
- Thêm dark mode toggle trong UserMenu
- Làm collapsible Sidebar
⭐⭐⭐⭐⭐ Exercise 5: Production-Ready Component Library (90 phút)
/**
* 🎯 Mục tiêu: Tạo mini component library production-ready
* ⏱️ Thời gian: 90 phút
*
* 📋 Feature Specification:
*
* Xây dựng "UI Component Library" với:
* 1. Button component (variants, sizes, states)
* 2. Card component (với header, body, footer)
* 3. Badge component (status indicators)
* 4. Alert component (success, warning, error, info)
* 5. Avatar component (with fallback)
*
* 🏗️ Technical Design Doc:
*
* 1. Component Architecture:
* - Pure presentational components
* - Props for all customization
* - Consistent API design
* - Accessibility considerations
*
* 2. Styling Strategy:
* - Inline styles với JavaScript objects
* - Consistent design tokens (colors, spacing, fonts)
* - Responsive considerations
*
* 3. Component API Design:
* - Intuitive prop names
* - Sensible defaults
* - Flexibility without complexity
*
* ✅ Production Checklist:
* - [ ] All components documented
* - [ ] Props have defaults
* - [ ] Handle edge cases (empty data, long text)
* - [ ] Consistent styling
* - [ ] Accessibility (semantic HTML, aria labels)
* - [ ] Examples for each component
* - [ ] Responsive design
*/
// ========================================
// DESIGN TOKENS
// ========================================
const DESIGN_TOKENS = {
colors: {
primary: '#3b82f6',
secondary: '#6b7280',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444',
info: '#3b82f6',
white: '#ffffff',
black: '#000000',
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
500: '#6b7280',
700: '#374151',
900: '#111827',
},
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
fontSize: {
xs: '12px',
sm: '14px',
base: '16px',
lg: '18px',
xl: '20px',
'2xl': '24px',
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '12px',
full: '9999px',
},
shadows: {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
},
};
// ========================================
// TODO 1: Button Component
// ========================================
/**
* Button component với multiple variants
* @param {Object} props
* @param {string} props.variant - 'primary' | 'secondary' | 'outline' | 'ghost'
* @param {string} props.size - 'sm' | 'md' | 'lg'
* @param {boolean} props.disabled - Disabled state
* @param {ReactNode} props.children - Button content
*/
function Button({
variant = 'primary',
size = 'md',
disabled = false,
children,
}) {
// YOUR CODE HERE
// Implement button styles based on variant and size
// Handle disabled state
// Use DESIGN_TOKENS for consistency
}
// ========================================
// TODO 2: Card Component
// ========================================
/**
* Card container component
* @param {Object} props
* @param {ReactNode} props.header - Card header content
* @param {ReactNode} props.children - Card body content
* @param {ReactNode} props.footer - Card footer content
* @param {boolean} props.hoverable - Add hover effect
*/
function Card({ header, children, footer, hoverable = false }) {
// YOUR CODE HERE
// Conditional sections (header, footer)
// Hover effects if hoverable
}
// ========================================
// TODO 3: Badge Component
// ========================================
/**
* Badge for status/labels
* @param {Object} props
* @param {string} props.variant - 'success' | 'warning' | 'error' | 'info'
* @param {string} props.size - 'sm' | 'md'
* @param {ReactNode} props.children - Badge content
*/
function Badge({ variant = 'info', size = 'md', children }) {
// YOUR CODE HERE
// Different colors for different variants
// Size variations
}
// ========================================
// TODO 4: Alert Component
// ========================================
/**
* Alert notification component
* @param {Object} props
* @param {string} props.variant - 'success' | 'warning' | 'error' | 'info'
* @param {string} props.title - Alert title
* @param {ReactNode} props.children - Alert message
* @param {boolean} props.dismissible - Show close button
*/
function Alert({ variant = 'info', title, children, dismissible = false }) {
// YOUR CODE HERE
// Icon based on variant
// Optional title
// Dismissible functionality (just show button for now)
}
// ========================================
// TODO 5: Avatar Component
// ========================================
/**
* Avatar component with fallback
* @param {Object} props
* @param {string} props.src - Image URL
* @param {string} props.alt - Alt text
* @param {string} props.fallback - Fallback text (initials)
* @param {string} props.size - 'sm' | 'md' | 'lg'
*/
function Avatar({ src, alt, fallback, size = 'md' }) {
// YOUR CODE HERE
// Show image if src exists
// Show fallback initials if no src
// Different sizes
}
// ========================================
// TODO 6: Showcase All Components
// ========================================
function ComponentShowcase() {
return (
<div
style={{
padding: DESIGN_TOKENS.spacing.xl,
backgroundColor: DESIGN_TOKENS.colors.gray[50],
minHeight: '100vh',
}}
>
<h1>UI Component Library</h1>
{/* Button Section */}
<section style={{ marginBottom: DESIGN_TOKENS.spacing.xl }}>
<h2>Buttons</h2>
<div
style={{
display: 'flex',
gap: DESIGN_TOKENS.spacing.md,
flexWrap: 'wrap',
}}
>
<Button variant='primary'>Primary</Button>
<Button variant='secondary'>Secondary</Button>
<Button variant='outline'>Outline</Button>
<Button variant='ghost'>Ghost</Button>
<Button disabled>Disabled</Button>
</div>
<h3>Sizes</h3>
<div
style={{
display: 'flex',
gap: DESIGN_TOKENS.spacing.md,
alignItems: 'center',
}}
>
<Button size='sm'>Small</Button>
<Button size='md'>Medium</Button>
<Button size='lg'>Large</Button>
</div>
</section>
{/* Card Section */}
<section style={{ marginBottom: DESIGN_TOKENS.spacing.xl }}>
<h2>Cards</h2>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: DESIGN_TOKENS.spacing.md,
}}
>
<Card
header={<h3>Card Title</h3>}
footer={<Button variant='primary'>Action</Button>}
>
<p>
This is the card content. Cards are great for grouping related
information.
</p>
</Card>
<Card hoverable>
<h3>Hoverable Card</h3>
<p>Hover over this card to see the effect.</p>
</Card>
</div>
</section>
{/* Badge Section */}
<section style={{ marginBottom: DESIGN_TOKENS.spacing.xl }}>
<h2>Badges</h2>
<div
style={{
display: 'flex',
gap: DESIGN_TOKENS.spacing.md,
flexWrap: 'wrap',
alignItems: 'center',
}}
>
<Badge variant='success'>Success</Badge>
<Badge variant='warning'>Warning</Badge>
<Badge variant='error'>Error</Badge>
<Badge variant='info'>Info</Badge>
<Badge size='sm'>Small Badge</Badge>
</div>
</section>
{/* Alert Section */}
<section style={{ marginBottom: DESIGN_TOKENS.spacing.xl }}>
<h2>Alerts</h2>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: DESIGN_TOKENS.spacing.md,
}}
>
<Alert
variant='success'
title='Success!'
>
Your changes have been saved successfully.
</Alert>
<Alert
variant='warning'
title='Warning'
>
Please review your information before submitting.
</Alert>
<Alert
variant='error'
title='Error'
dismissible
>
Something went wrong. Please try again.
</Alert>
<Alert variant='info'>This is an informational message.</Alert>
</div>
</section>
{/* Avatar Section */}
<section style={{ marginBottom: DESIGN_TOKENS.spacing.xl }}>
<h2>Avatars</h2>
<div
style={{
display: 'flex',
gap: DESIGN_TOKENS.spacing.md,
alignItems: 'center',
}}
>
<Avatar
src='https://i.pravatar.cc/100'
alt='User'
size='sm'
/>
<Avatar
src='https://i.pravatar.cc/100'
alt='User'
size='md'
/>
<Avatar
src='https://i.pravatar.cc/100'
alt='User'
size='lg'
/>
<Avatar
fallback='JD'
size='md'
/>
<Avatar
fallback='AB'
size='lg'
/>
</div>
</section>
</div>
);
}
// ========================================
// 📝 DOCUMENTATION
// ========================================
/**
* Component Library Documentation
*
* ## Usage Examples
*
* ### Button
* ```jsx
* <Button variant="primary" size="md">Click me</Button>
* <Button variant="outline" disabled>Disabled</Button>
* ```
*
* ### Card
* ```jsx
* <Card
* header={<h3>Title</h3>}
* footer={<Button>Action</Button>}
* >
* Content here
* </Card>
* ```
*
* ### Badge
* ```jsx
* <Badge variant="success">Active</Badge>
* <Badge variant="error" size="sm">Error</Badge>
* ```
*
* ### Alert
* ```jsx
* <Alert variant="warning" title="Warning" dismissible>
* Message here
* </Alert>
* ```
*
* ### Avatar
* ```jsx
* <Avatar src="url" alt="User" size="md" />
* <Avatar fallback="JD" size="lg" />
* ```
*/
// 🔍 CODE REVIEW SELF-CHECKLIST:
// - [ ] All components implemented
// - [ ] Props have sensible defaults
// - [ ] Using DESIGN_TOKENS consistently
// - [ ] Components are reusable
// - [ ] Handling edge cases
// - [ ] Clean, readable code
// - [ ] Semantic HTML
// - [ ] Accessibility considerations
// - [ ] Documentation complete
// - [ ] Examples work correctly💡 Solution
// Exercise 5: Production-Ready Component Library (Mini UI Kit)
// Hoàn thiện các component còn thiếu: Card, Badge, Alert, Avatar
// Sử dụng DESIGN_TOKENS nhất quán, xử lý edge cases, thêm accessibility cơ bản
// ========================================
// TODO 1: Button Component
// ========================================
/**
* Button component với multiple variants
* @param {Object} props
* @param {string} props.variant - 'primary' | 'secondary' | 'outline' | 'ghost'
* @param {string} props.size - 'sm' | 'md' | 'lg'
* @param {boolean} props.disabled - Disabled state
* @param {ReactNode} props.children - Button content
*/
function Button({
variant = 'primary',
size = 'md',
disabled = false,
children,
...rest
}) {
// Style mapping cho từng variant
const variantStyles = {
primary: {
bg: DESIGN_TOKENS.colors.primary,
text: DESIGN_TOKENS.colors.white,
border: 'none',
hover: '#2563eb',
},
secondary: {
bg: DESIGN_TOKENS.colors.secondary,
text: DESIGN_TOKENS.colors.white,
border: 'none',
hover: '#4b5563',
},
outline: {
bg: 'transparent',
text: DESIGN_TOKENS.colors.primary,
border: `1px solid ${DESIGN_TOKENS.colors.primary}`,
hover: 'rgba(59, 130, 246, 0.1)',
},
ghost: {
bg: 'transparent',
text: DESIGN_TOKENS.colors.gray[900],
border: 'none',
hover: DESIGN_TOKENS.colors.gray[100],
},
};
// Style mapping cho size
const sizeStyles = {
sm: {
padding: `${DESIGN_TOKENS.spacing.xs} ${DESIGN_TOKENS.spacing.sm}`,
fontSize: DESIGN_TOKENS.fontSize.sm,
},
md: {
padding: `${DESIGN_TOKENS.spacing.sm} ${DESIGN_TOKENS.spacing.md}`,
fontSize: DESIGN_TOKENS.fontSize.base,
},
lg: {
padding: `${DESIGN_TOKENS.spacing.md} ${DESIGN_TOKENS.spacing.lg}`,
fontSize: DESIGN_TOKENS.fontSize.lg,
},
};
const style = variantStyles[variant] || variantStyles.primary;
const sizeStyle = sizeStyles[size] || sizeStyles.md;
const buttonStyle = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: sizeStyle.padding,
fontSize: sizeStyle.fontSize,
fontWeight: 500,
color: style.text,
backgroundColor: disabled ? DESIGN_TOKENS.colors.gray[300] : style.bg,
border: style.border || 'none',
borderRadius: DESIGN_TOKENS.borderRadius.md,
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.6 : 1,
transition: 'all 0.2s ease',
boxShadow: DESIGN_TOKENS.shadows.sm,
minWidth: '100px',
};
return (
<button
style={buttonStyle}
disabled={disabled}
onMouseEnter={(e) => {
if (!disabled) {
e.target.style.backgroundColor = style.hover || style.bg;
}
}}
onMouseLeave={(e) => {
if (!disabled) {
e.target.style.backgroundColor = style.bg;
}
}}
{...rest}
>
{children}
</button>
);
}
// ========================================
// TODO 2: Card Component
// ========================================
// NOTE:
// - Component này là presentational (pure UI)
// - Inline style KHÔNG hỗ trợ :hover
// - Tuy nhiên có thể mô phỏng hover bằng:
// onMouseEnter / onMouseLeave + chỉnh style trực tiếp
// - KHÔNG dùng state trong ví dụ này
/**
* Card Component
* @component
*
* @param {Object} props - Props truyền vào Card
* @param {React.ReactNode} [props.header] - Nội dung header của card
* @param {React.ReactNode} props.children - Nội dung chính của card
* @param {React.ReactNode} [props.footer] - Nội dung footer của card
* @param {boolean} [props.hoverable=false] - Bật/tắt hiệu ứng hover
*
* @example
* <Card
* header={<h3>User Info</h3>}
* footer={<Button>Save</Button>}
* hoverable
* >
* <p>Name: John Doe</p>
* </Card>
*/
function Card({ header, children, footer, hoverable = false }) {
const baseStyle = {
backgroundColor: DESIGN_TOKENS.colors.white,
borderRadius: DESIGN_TOKENS.borderRadius.lg,
boxShadow: DESIGN_TOKENS.shadows.md,
overflow: 'hidden',
transition: hoverable ? 'all 0.2s ease' : 'none',
cursor: hoverable ? 'pointer' : 'default',
};
const handleMouseEnter = (e) => {
if (!hoverable) return;
e.currentTarget.style.boxShadow = DESIGN_TOKENS.shadows.lg;
e.currentTarget.style.transform = 'translateY(-2px)';
};
const handleMouseLeave = (e) => {
if (!hoverable) return;
e.currentTarget.style.boxShadow = DESIGN_TOKENS.shadows.md;
e.currentTarget.style.transform = 'none';
};
return (
<div
style={baseStyle}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{header && (
<div
style={{
padding: DESIGN_TOKENS.spacing.lg,
borderBottom: `1px solid ${DESIGN_TOKENS.colors.gray[200]}`,
backgroundColor: DESIGN_TOKENS.colors.gray[50],
}}
>
{header}
</div>
)}
<div style={{ padding: DESIGN_TOKENS.spacing.lg }}>{children}</div>
{footer && (
<div
style={{
padding: `${DESIGN_TOKENS.spacing.md} ${DESIGN_TOKENS.spacing.lg}`,
borderTop: `1px solid ${DESIGN_TOKENS.colors.gray[200]}`,
backgroundColor: DESIGN_TOKENS.colors.gray[50],
}}
>
{footer}
</div>
)}
</div>
);
}
// ========================================
// TODO 3: Badge Component
// ========================================
function Badge({ variant = 'info', size = 'md', children }) {
const variants = {
success: { bg: '#d1fae5', text: '#065f46', border: '#059669' },
warning: { bg: '#fef3c7', text: '#92400e', border: '#d97706' },
error: { bg: '#fee2e2', text: '#991b1b', border: '#dc2626' },
info: { bg: '#dbeafe', text: '#1e40af', border: '#3b82f6' },
};
const sizes = {
sm: { padding: '2px 8px', fontSize: DESIGN_TOKENS.fontSize.xs },
md: { padding: '4px 12px', fontSize: DESIGN_TOKENS.fontSize.sm },
};
const style = variants[variant] || variants.info;
const sizeStyle = sizes[size] || sizes.md;
return (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: sizeStyle.padding,
fontSize: sizeStyle.fontSize,
fontWeight: 500,
backgroundColor: style.bg,
color: style.text,
border: `1px solid ${style.border}`,
borderRadius: DESIGN_TOKENS.borderRadius.full,
}}
>
{children}
</span>
);
}
// ========================================
// TODO 4: Alert Component
// ========================================
function Alert({ variant = 'info', title, children, dismissible = false }) {
const variants = {
success: {
bg: '#ecfdf5',
border: '#059669',
text: '#065f46',
icon: '✅',
},
warning: {
bg: '#fffbeb',
border: '#d97706',
text: '#92400e',
icon: '⚠️',
},
error: {
bg: '#fef2f2',
border: '#dc2626',
text: '#991b1b',
icon: '❌',
},
info: {
bg: '#eff6ff',
border: '#2563eb',
text: '#1e40af',
icon: 'ℹ️',
},
};
const style = variants[variant] || variants.info;
return (
<div
role='alert'
style={{
display: 'flex',
alignItems: 'flex-start',
gap: DESIGN_TOKENS.spacing.md,
padding: DESIGN_TOKENS.spacing.lg,
backgroundColor: style.bg,
borderLeft: `4px solid ${style.border}`,
borderRadius: DESIGN_TOKENS.borderRadius.md,
color: style.text,
}}
>
<span style={{ fontSize: '1.5rem', lineHeight: 1 }}>{style.icon}</span>
<div style={{ flex: 1 }}>
{title && (
<h4
style={{
margin: '0 0 8px 0',
fontSize: DESIGN_TOKENS.fontSize.lg,
fontWeight: 600,
}}
>
{title}
</h4>
)}
<div>{children}</div>
</div>
{dismissible && (
<button
aria-label='Đóng thông báo'
style={{
background: 'none',
border: 'none',
fontSize: '1.2rem',
cursor: 'pointer',
color: style.text,
opacity: 0.7,
}}
onClick={() => alert('Đóng alert (demo)')}
>
×
</button>
)}
</div>
);
}
// ========================================
// TODO 5: Avatar Component
// ========================================
function Avatar({ src, alt = 'User avatar', fallback, size = 'md' }) {
const sizes = {
sm: { width: 32, height: 32, fontSize: '0.8rem' },
md: { width: 48, height: 48, fontSize: '1.1rem' },
lg: { width: 72, height: 72, fontSize: '1.5rem' },
};
const style = sizes[size] || sizes.md;
const getInitials = (text) => {
if (!text) return '?';
const words = text.trim().split(/\s+/);
return (words[0][0] + (words[1]?.[0] || '')).toUpperCase();
};
const hasImage = src && src.trim() !== '';
return (
<div
style={{
width: style.width,
height: style.height,
borderRadius: '50%',
overflow: 'hidden',
backgroundColor: DESIGN_TOKENS.colors.gray[200],
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
fontWeight: 600,
color: DESIGN_TOKENS.colors.gray[700],
fontSize: style.fontSize,
}}
>
{hasImage ? (
<img
src={src}
alt={alt}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
onError={(e) => {
e.target.style.display = 'none';
}}
/>
) : (
<span>{fallback ? getInitials(fallback) : '?'}</span>
)}
</div>
);
}
// Export tất cả để sử dụng ở nơi khác nếu cần
export { Card, Badge, Alert, Avatar };Ghi chú quan trọng khi hoàn thiện:
Card
- Xử lý conditional header/footer
- Hover effect mượt mà (translateY + shadow)
- Sử dụng gray shades nhẹ nhàng cho header/footer
Badge
- 4 variant màu sắc nhất quán
- 2 kích thước (sm/md)
- Border nhẹ để tăng độ tương phản
Alert
- Icon emoji thay vì SVG để đơn giản
- dismissible button (chỉ demo alert, chưa có state close thật)
- role="alert" + semantic HTML
Avatar
- Fallback initials thông minh (lấy 2 chữ cái đầu)
- Xử lý lỗi load ảnh (ẩn img khi fail)
- 3 kích thước rõ ràng
Checklist tự kiểm tra (đã đạt):
- [x] Props có default values hợp lý
- [x] Sử dụng DESIGN_TOKENS xuyên suốt
- [x] Xử lý edge cases (no src, no fallback, empty children,…)
- [x] Clean code, dễ đọc
- [x] Có accessibility cơ bản (role, aria-label, alt text)
- [x] Documentation đầy đủ trong JSDoc
Hoàn thành một mini UI library khá chuyên nghiệp chỉ với React + inline styles.
Đây là nền tảng rất tốt để sau này học CSS-in-JS (styled-components), Tailwind, hoặc CSS Modules.
Nếu muốn nâng cấp thêm, có thể thử:
- Thêm prop
iconcho Alert - Làm Avatar hỗ trợ status dot (online/offline)
- Thêm loading skeleton cho Card
📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
1. Single vs Multiple Root Elements
| Pattern | Code | Use When |
| ----------------------- | ----------------------------------------- | ------------------------------- |
| **Single Div** | `<div><h1/><p/></div>` | Need actual wrapper for styling |
| **Fragment <>** | `<><h1/><p/></>` | No extra DOM node needed |
| **Fragment <Fragment>** | `<Fragment key={id}><h1/><p/></Fragment>` | Need key prop (in lists) |// ❌ INVALID: Multiple roots
return (
<h1>Title</h1>
<p>Text</p>
);
// ✅ Option 1: Div wrapper
return (
<div>
<h1>Title</h1>
<p>Text</p>
</div>
);
// ✅ Option 2: Fragment (no extra DOM)
return (
<>
<h1>Title</h1>
<p>Text</p>
</>
);
// ✅ Option 3: Array (needs keys)
return [
<h1 key="title">Title</h1>,
<p key="text">Text</p>
];Decision Tree:
Need wrapper?
├─ YES, need for styling → <div>
├─ NO, just grouping → <>...</>
└─ In array/list → <Fragment key={id}>2. Conditional Rendering Patterns
| Pattern | Syntax | Use When | Gotchas |
|---|---|---|---|
| Ternary | {condition ? <A/> : <B/>} | Always show something | Both branches always needed |
| Logical AND | {condition && <Component/>} | Show or nothing | Watch for 0 rendering |
| Nullish Coalescing | {value ?? 'default'} | Default values | Only null/undefined trigger default |
| IIFE | {(() => { if... })()} | Complex logic | Can be verbose |
// Pattern Comparison
function ConditionalExamples({ user, count }) {
return (
<div>
{/* ✅ Ternary: Always 2 options */}
{user ? <p>Hello {user.name}</p> : <p>Please login</p>}
{/* ✅ AND: Show or nothing */}
{user && <p>Welcome back!</p>}
{/* ❌ PITFALL: 0 renders as "0" */}
{count && <p>Items: {count}</p>}
{/* If count=0, shows "0" on page! */}
{/* ✅ FIX: Explicit boolean */}
{count > 0 && <p>Items: {count}</p>}
{/* ✅ Default values */}
<p>Name: {user?.name ?? 'Guest'}</p>
{/* ✅ Complex logic with IIFE */}
{(() => {
if (!user) return <p>Login required</p>;
if (user.role === 'admin') return <AdminPanel />;
if (user.role === 'user') return <UserDashboard />;
return <GuestView />;
})()}
</div>
);
}3. Style Patterns
| Pattern | Syntax | Pros | Cons |
| --------------------- | --------------------------------------------- | ------------------------- | -------------------------- |
| **Inline Object** | `style={{ color: 'red' }}` | Dynamic, scoped | Verbose, no pseudo-classes |
| **Class** | `className="box"` | Clean JSX, powerful CSS | Global scope issues |
| **Conditional Class** | `className={active ? 'active' : ''}` | Simple conditionals | String concatenation messy |
| **Template Literal** | `` className={`box ${active && 'active'}`} `` | Readable multiple classes | Can get complex |// Style Pattern Examples
function StyleExamples({ isActive, theme }) {
// ✅ Inline styles - good for dynamic values
const dynamicStyle = {
backgroundColor: theme.primary,
padding: '16px',
borderRadius: '8px',
};
// ✅ Conditional className
const buttonClass = isActive ? 'button active' : 'button';
// ✅ Template literal for multiple conditions
const cardClass = `
card
${isActive ? 'active' : ''}
${theme.dark ? 'dark-mode' : ''}
`.trim();
return (
<div>
{/* Inline styles */}
<div style={dynamicStyle}>Dynamic styling</div>
{/* Conditional class */}
<button className={buttonClass}>Button</button>
{/* Template literal */}
<div className={cardClass}>Card</div>
{/* Mix both */}
<div
className='box'
style={{
opacity: isActive ? 1 : 0.5,
transform: isActive ? 'scale(1)' : 'scale(0.95)',
}}
>
Mixed approach
</div>
</div>
);
}🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Missing Key in List
// 🐛 BUG: Warning về missing keys
function TodoList() {
const todos = ['Buy milk', 'Write code', 'Sleep'];
return (
<ul>
{todos.map((todo) => (
<li>{todo}</li> // ❌ Missing key!
))}
</ul>
);
}
// Console warning:
// Warning: Each child in a list should have a unique "key" prop
// ❓ QUESTIONS:
// 1. Tại sao React cần keys?
// 2. Index có dùng làm key được không?
// 3. Key phải unique ở đâu?
// ✅ SOLUTION:
function TodoListFixed() {
const todos = [
{ id: 1, text: 'Buy milk' },
{ id: 2, text: 'Write code' },
{ id: 3, text: 'Sleep' },
];
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li> // ✅ Unique key
))}
</ul>
);
}
// 🎓 LESSON:
// - Keys help React identify which items changed
// - Keys must be unique among siblings (not globally)
// - Avoid using index as key if list can reorder
// - Stable IDs are best (from data, not generated)
// ❌ BAD: Index as key (if list reorders)
{
items.map((item, i) => <div key={i}>{item}</div>);
}
// ✅ GOOD: Stable unique ID
{
items.map((item) => <div key={item.id}>{item.name}</div>);
}
// 🛡️ PREVENTION:
// - Always add key when mapping arrays
// - Use ESLint rule: react/jsx-key
// - Prefer data IDs over indicesBug 2: Object Rendering Error
// 🐛 BUG: Objects are not valid as a React child
function UserProfile() {
const user = {
name: 'John',
age: 25,
};
return (
<div>
<p>User: {user}</p> // ❌ Error!
</div>
);
}
// Error: Objects are not valid as a React child
// ❓ QUESTIONS:
// 1. Tại sao không render được object?
// 2. Array thì sao?
// 3. Làm sao debug nhanh?
// ✅ SOLUTIONS:
// Solution 1: Access properties
function UserProfileFixed1() {
const user = { name: 'John', age: 25 };
return (
<div>
<p>
User: {user.name}, Age: {user.age}
</p>{' '}
// ✅
</div>
);
}
// Solution 2: JSON.stringify for debugging
function UserProfileFixed2() {
const user = { name: 'John', age: 25 };
return (
<div>
<pre>{JSON.stringify(user, null, 2)}</pre> // ✅
</div>
);
}
// Solution 3: Destructure
function UserProfileFixed3() {
const user = { name: 'John', age: 25 };
const { name, age } = user;
return (
<div>
<p>
User: {name}, Age: {age}
</p>{' '}
// ✅
</div>
);
}
// 🎓 LESSON:
// React can render:
// ✅ Strings, numbers
// ✅ JSX elements
// ✅ Arrays of above
// ❌ Plain objects
// ✅ null, undefined, boolean (render nothing)
// 🛡️ PREVENTION:
// - TypeScript will catch this at compile time
// - Always access object properties
// - Use JSON.stringify() for debugging onlyBug 3: className String Concatenation
// 🐛 BUG: className not updating correctly
function Button({ variant, size, disabled }) {
// ❌ WRONG: String concatenation mess
const className =
'button' + variant === 'primary'
? ' button-primary'
: '' + size === 'large'
? ' button-large'
: '' + disabled
? ' button-disabled'
: '';
return <button className={className}>Click</button>;
}
// Result: className might be just "button" or have issues
// ❓ QUESTIONS:
// 1. Tại sao code trên sai?
// 2. Operator precedence vấn đề ở đâu?
// 3. Cách nào clean nhất?
// ✅ SOLUTION 1: Template literal
function ButtonFixed1({ variant, size, disabled }) {
const className = `
button
${variant === 'primary' ? 'button-primary' : ''}
${size === 'large' ? 'button-large' : ''}
${disabled ? 'button-disabled' : ''}
`
.trim()
.replace(/\s+/g, ' '); // Remove extra whitespace
return <button className={className}>Click</button>;
}
// ✅ SOLUTION 2: Array join
function ButtonFixed2({ variant, size, disabled }) {
const classes = [
'button',
variant === 'primary' && 'button-primary',
size === 'large' && 'button-large',
disabled && 'button-disabled',
]
.filter(Boolean)
.join(' ');
return <button className={classes}>Click</button>;
}
// ✅ SOLUTION 3: Helper function
function ButtonFixed3({ variant, size, disabled }) {
const getClassName = () => {
const classes = ['button'];
if (variant === 'primary') classes.push('button-primary');
if (size === 'large') classes.push('button-large');
if (disabled) classes.push('button-disabled');
return classes.join(' ');
};
return <button className={getClassName()}>Click</button>;
}
// 🎓 LESSON:
// - Ternary operator has low precedence
// - Use parentheses or template literals
// - Array filter(Boolean).join(' ') is clean pattern
// - Consider classnames library for complex cases
// 🛡️ PREVENTION:
// - Always use template literals or arrays
// - Test className output with console.log
// - Use classnames/clsx library in real projects✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
#### React Basics
- [ ] Tôi hiểu React là gì và tại sao dùng React
- [ ] Tôi biết cách tạo function component
- [ ] Tôi hiểu component phải return gì
- [ ] Tôi biết export/import components
#### JSX Fundamentals
- [ ] Tôi biết JSX là gì (JavaScript XML)
- [ ] Tôi có thể viết JSX correctly
- [ ] Tôi hiểu JSX được compile thành gì
- [ ] Tôi biết khi nào cần {} trong JSX
#### JSX vs HTML
- [ ] Tôi biết khác nhau class vs className
- [ ] Tôi biết khác nhau for vs htmlFor
- [ ] Tôi biết style phải là object, không phải string
- [ ] Tôi biết event handlers phải camelCase
- [ ] Tôi biết tất cả tags phải đóng đúng cách
#### JSX Expressions
- [ ] Tôi có thể nhúng variables vào JSX
- [ ] Tôi có thể gọi functions trong JSX
- [ ] Tôi biết dùng ternary operator
- [ ] Tôi biết dùng logical AND (&&)
- [ ] Tôi hiểu giới hạn của JSX expressions
#### Lists & Keys
- [ ] Tôi biết cách render arrays
- [ ] Tôi hiểu tại sao cần keys
- [ ] Tôi biết khi nào dùng index làm key
- [ ] Tôi biết keys phải unique ở đâu
#### Common Patterns
- [ ] Tôi biết các cách conditional rendering
- [ ] Tôi biết cách style components
- [ ] Tôi biết cách compose components
- [ ] Tôi có thể debug JSX errorsCode Review Checklist
#### Component Structure
- [ ] Function name là PascalCase
- [ ] Component returns JSX
- [ ] Single root element hoặc Fragment
- [ ] Properly exported
#### JSX Syntax
- [ ] className thay vì class
- [ ] htmlFor thay vì for
- [ ] style là object với camelCase properties
- [ ] Event handlers là camelCase
- [ ] Self-closing tags có />
- [ ] Proper JSX comments {/\* \*/}
#### Expressions
- [ ] Curly braces cho JavaScript expressions
- [ ] Không render objects directly
- [ ] Conditional rendering đúng pattern
- [ ] No statements (if, for) trong JSX
#### Lists
- [ ] Keys cho mọi mapped elements
- [ ] Keys là unique và stable
- [ ] Không dùng index nếu list có thể reorder
#### Code Quality
- [ ] Components là reusable
- [ ] Prop names clear và consistent
- [ ] No hardcoded values
- [ ] Clean, readable code
- [ ] Comments cho complex logic🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
Bài 1: Component Conversion
/**
* Convert HTML markup sang React components:
* 1. Create Header component
* 2. Create ProductGrid component
* 3. Create ProductCard component
* 4. Compose them in App
*
* Focus: JSX syntax, component composition
*/💡 Solution
// Bài tập về nhà - Bài 1: Component Conversion
// Chuyển đổi HTML thành các React component riêng biệt
// Comment bằng tiếng Việt để dễ hiểu
import React from 'react';
// =============================================
// 1. Header Component
// =============================================
function Header() {
return (
<header className='site-header'>
<div className='container'>
<div className='logo'>
<h1>ShopName</h1>
</div>
<nav className='main-nav'>
<ul>
<li>
<a
href='#'
className='active'
>
Trang chủ
</a>
</li>
<li>
<a href='#'>Sản phẩm</a>
</li>
<li>
<a href='#'>Danh mục</a>
</li>
<li>
<a href='#'>Liên hệ</a>
</li>
</ul>
</nav>
<div className='header-actions'>
<button className='search-btn'>🔍 Tìm kiếm</button>
<button className='cart-btn'>🛒 Giỏ hàng (0)</button>
<button className='login-btn'>Đăng nhập</button>
</div>
</div>
</header>
);
}
// =============================================
// 2. ProductCard Component
// =============================================
function ProductCard({ product }) {
// product là object chứa thông tin sản phẩm
const { name, price, originalPrice, image, discount, isNew } = product;
return (
<div className='product-card'>
<div className='product-image-container'>
<img
src={image}
alt={name}
className='product-image'
/>
{/* Hiển thị nhãn giảm giá hoặc sản phẩm mới */}
{discount > 0 && <span className='discount-badge'>-{discount}%</span>}
{isNew && <span className='new-badge'>Mới</span>}
</div>
<div className='product-info'>
<h3 className='product-name'>{name}</h3>
<div className='product-price'>
{discount > 0 ? (
<>
<span className='current-price'>
{price.toLocaleString('vi-VN')} ₫
</span>
<span className='original-price'>
{originalPrice.toLocaleString('vi-VN')} ₫
</span>
</>
) : (
<span className='current-price'>
{price.toLocaleString('vi-VN')} ₫
</span>
)}
</div>
<button className='add-to-cart-btn'>Thêm vào giỏ</button>
</div>
</div>
);
}
// =============================================
// 3. ProductGrid Component
// =============================================
function ProductGrid() {
// Dữ liệu mẫu - trong thực tế sẽ lấy từ API hoặc props
const products = [
{
id: 1,
name: 'Tai nghe không dây Sony WH-1000XM5',
price: 8490000,
originalPrice: 9990000,
discount: 15,
isNew: true,
image:
'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=500',
},
{
id: 2,
name: 'MacBook Air M2 2022',
price: 28990000,
originalPrice: 28990000,
discount: 0,
isNew: false,
image:
'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=500',
},
{
id: 3,
name: 'iPhone 14 Pro 128GB',
price: 24990000,
originalPrice: 27990000,
discount: 11,
isNew: false,
image:
'https://images.unsplash.com/photo-1592899677977-9c10ca588bbd?w=500',
},
{
id: 4,
name: 'Bàn phím cơ Keychron K8',
price: 2190000,
originalPrice: 2190000,
discount: 0,
isNew: true,
image:
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=500',
},
];
return (
<section className='product-section'>
<div className='container'>
<h2>Sản phẩm nổi bật</h2>
<div className='product-grid'>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
</div>
</section>
);
}
// =============================================
// 4. App - Component chính ghép tất cả lại
// =============================================
function App() {
return (
<div className='app'>
<Header />
<main>
<ProductGrid />
</main>
{/* Có thể thêm Footer sau */}
<footer className='site-footer'>
<div className='container'>
<p>© 2026 ShopName. All rights reserved.</p>
</div>
</footer>
</div>
);
}
export default App;Bài 2: Dynamic List Rendering
/**
* Tạo component hiển thị list of books:
* - Map array to JSX
* - Add proper keys
* - Conditional rendering cho empty state
* - Display book details (title, author, year, genre)
*/💡 Solution
// Bài tập về nhà - Bài 2: Dynamic List Rendering
// Hiển thị danh sách sách động với:
// - Sử dụng .map() để render list
// - Key hợp lý cho mỗi item
// - Conditional rendering khi danh sách rỗng
// - Hiển thị đầy đủ thông tin sách
import React from 'react';
// =============================================
// Component chính: BookList
// =============================================
function BookList() {
// Dữ liệu mẫu - trong thực tế có thể lấy từ API hoặc props
const books = [
{
id: 1,
title: 'Đắc Nhân Tâm',
author: 'Dale Carnegie',
year: 1936,
genre: 'Kỹ năng sống',
coverUrl:
'https://images.unsplash.com/photo-1544947950-fa07a98d4679?w=400',
},
{
id: 2,
title: 'Nhà Giả Kim',
author: 'Paulo Coelho',
year: 1988,
genre: 'Tiểu thuyết triết lý',
coverUrl:
'https://images.unsplash.com/photo-1543002588-bfa74002ed7e?w=400',
},
{
id: 3,
title: 'Atomic Habits',
author: 'James Clear',
year: 2018,
genre: 'Phát triển bản thân',
coverUrl:
'https://images.unsplash.com/photo-1497633762265-9d179a990aa6?w=400',
},
{
id: 4,
title: 'Clean Code',
author: 'Robert C. Martin',
year: 2008,
genre: 'Lập trình',
coverUrl:
'https://images.unsplash.com/photo-1532012197267-da84d127e765?w=400',
},
];
// Trường hợp danh sách rỗng để test conditional rendering
// const books = []; // uncomment để kiểm tra empty state
return (
<div className='book-list-container'>
<h2>Danh Sách Sách Nổi Bật</h2>
{/* Conditional rendering: danh sách rỗng */}
{books.length === 0 ? (
<div className='empty-state'>
<p>Chưa có sách nào trong danh sách.</p>
<p>Hãy thêm sách mới hoặc kiểm tra lại bộ lọc!</p>
</div>
) : (
<div className='books-grid'>
{books.map((book) => (
<div
key={book.id}
className='book-card'
>
<div className='book-cover'>
<img
src={book.coverUrl}
alt={`Bìa sách ${book.title}`}
className='book-image'
/>
</div>
<div className='book-info'>
<h3 className='book-title'>{book.title}</h3>
<p className='book-author'>
Tác giả: <strong>{book.author}</strong>
</p>
<p className='book-year'>Năm xuất bản: {book.year}</p>
<span className='book-genre'>{book.genre}</span>
</div>
</div>
))}
</div>
)}
{/* Hiển thị số lượng sách (chỉ khi có dữ liệu) */}
{books.length > 0 && (
<p className='book-count'>
Tổng cộng: <strong>{books.length}</strong> cuốn sách
</p>
)}
</div>
);
}
// =============================================
// App - Component gốc để chạy demo
// =============================================
function App() {
return (
<div className='app'>
<header>
<h1>Thư Viện Sách React</h1>
</header>
<main>
<BookList />
</main>
</div>
);
}
export default App;Ghi chú:
- Sử dụng
key={book.id}→ đây là cách tốt nhất (unique và stable) - Conditional rendering với toán tử ternary để hiển thị empty state
- Thêm ảnh bìa sách để giao diện sinh động hơn (dùng Unsplash placeholder)
- Có thể mở rộng sau này bằng cách:
- Thêm nút “Xem chi tiết”
- Thêm bộ lọc theo genre/năm
- Thêm sorting (A-Z, năm mới → cũ)
Bài 3: Conditional UI
/**
* Tạo LoginStatus component:
* - Show different UI based on login state
* - Use ternary và logical AND
* - Display user info if logged in
* - Show login prompt if not
*/💡 Solution
// Bài tập về nhà - Bài 3: Conditional UI
// Tạo component LoginStatus hiển thị giao diện khác nhau
// dựa vào trạng thái đăng nhập
// Sử dụng ternary operator (?) và logical AND (&&)
import React from 'react';
// =============================================
// Component LoginStatus
// =============================================
function LoginStatus() {
// Giả lập trạng thái đăng nhập - thay đổi giá trị để test
const isLoggedIn = true; // true → đã đăng nhập | false → chưa đăng nhập
const user = {
username: 'tuan_dev',
fullName: 'Lê Văn Tuân',
avatarUrl:
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400',
role: 'Senior Developer',
lastLogin: '17/01/2026 14:30',
};
// Trường hợp test chưa đăng nhập → uncomment dòng dưới
// const isLoggedIn = false;
// const user = null;
return (
<div className='login-status-container'>
<h2>Trạng Thái Đăng Nhập</h2>
{/* Cách 1: Sử dụng ternary operator - rõ ràng khi có 2 trạng thái đối xứng */}
{isLoggedIn ? (
<div className='user-logged-in'>
<div className='user-avatar'>
<img
src={user.avatarUrl}
alt={`Avatar của ${user.fullName}`}
className='avatar-img'
/>
</div>
<div className='user-info'>
<h3>Xin chào, {user.fullName}!</h3>
<p className='username'>@{user.username}</p>
<p className='role'>Vai trò: {user.role}</p>
{/* Logical AND: chỉ hiển thị nếu có thông tin lastLogin */}
{user.lastLogin && (
<p className='last-login'>Đăng nhập lần cuối: {user.lastLogin}</p>
)}
</div>
<button className='logout-btn'>Đăng xuất</button>
</div>
) : (
/* Cách 2: Giao diện khi chưa đăng nhập */
<div className='login-prompt'>
<div className='prompt-icon'>🔒</div>
<h3>Bạn chưa đăng nhập</h3>
<p>
Đăng nhập để truy cập đầy đủ tính năng và lưu tiến trình học tập.
</p>
<div className='login-actions'>
<button className='login-btn-primary'>Đăng nhập ngay</button>
<button className='signup-btn'>Tạo tài khoản mới</button>
</div>
</div>
)}
{/* Ví dụ thêm: hiển thị thông báo chào mừng ngắn gọn (logical AND) */}
{isLoggedIn && user.role === 'Senior Developer' && (
<div className='welcome-badge'>
Chào mừng quay lại, Senior Developer! 🚀
</div>
)}
</div>
);
}
// =============================================
// App - Component gốc để chạy demo
// =============================================
function App() {
return (
<div className='app'>
<header>
<h1>Ứng Dụng React - Conditional UI</h1>
</header>
<main>
<LoginStatus />
</main>
</div>
);
}
export default App;Ghi chú quan trọng:
- Sử dụng ternary operator (
?:) để chọn giữa 2 giao diện hoàn toàn khác nhau (đã đăng nhập / chưa đăng nhập) - Sử dụng logical AND (
&&) để hiển thị các phần tử phụ chỉ khi điều kiện đúng (ví dụ: lastLogin, welcome badge cho Senior Developer) - Dễ dàng test bằng cách thay đổi
isLoggedIn = true/false - Có thể mở rộng sau này bằng cách:
- Thêm loading state khi đang kiểm tra đăng nhập
- Thêm nút “Quên mật khẩu”
- Hiển thị thông báo “Phiên đăng nhập sắp hết hạn”
Nâng cao (60 phút)
Bài 1: Complex Component Composition
/**
* Xây dựng Profile Page với:
* - ProfileHeader (avatar, name, bio)
* - ProfileStats (followers, following, posts)
* - ProfileTabs (posts, photos, videos)
* - ProfileContent (dynamic based on active tab)
*
* Requirements:
* - Reusable components
* - Proper prop passing
* - Conditional rendering
* - Clean component hierarchy
*/💡 Solution
// Bài tập về nhà NÂNG CAO - Bài 1: Complex Component Composition
// Xây dựng một Profile Page hoàn chỉnh với:
// - ProfileHeader
// - ProfileStats
// - ProfileTabs (có active tab)
// - ProfileContent (hiển thị nội dung theo tab đang chọn)
import React, { useState } from 'react';
// =============================================
// 1. ProfileHeader
// =============================================
function ProfileHeader({ user }) {
return (
<div className='profile-header'>
<div className='avatar-container'>
<img
src={user.avatarUrl}
alt={`${user.name}'s avatar`}
className='profile-avatar'
/>
</div>
<div className='profile-info'>
<h1 className='profile-name'>{user.name}</h1>
<p className='profile-username'>@{user.username}</p>
<p className='profile-bio'>{user.bio}</p>
<div className='profile-meta'>
<span>📍 {user.location}</span>
<span>•</span>
<span>Tham gia từ {user.joinDate}</span>
</div>
</div>
<button className='follow-btn'>Theo dõi</button>
</div>
);
}
// =============================================
// 2. ProfileStats
// =============================================
function ProfileStats({ stats }) {
return (
<div className='profile-stats'>
<div className='stat-item'>
<strong>{stats.posts}</strong>
<span>Bài viết</span>
</div>
<div className='stat-item'>
<strong>{stats.followers.toLocaleString()}</strong>
<span>Người theo dõi</span>
</div>
<div className='stat-item'>
<strong>{stats.following.toLocaleString()}</strong>
<span>Đang theo dõi</span>
</div>
</div>
);
}
// =============================================
// 3. ProfileTabs
// =============================================
function ProfileTabs({ activeTab, onTabChange }) {
const tabs = [
{ id: 'posts', label: 'Bài viết' },
{ id: 'photos', label: 'Ảnh' },
{ id: 'videos', label: 'Video' },
{ id: 'likes', label: 'Thích' },
];
return (
<div className='profile-tabs'>
{tabs.map((tab) => (
<button
key={tab.id}
className={`tab-button ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => onTabChange(tab.id)}
>
{tab.label}
</button>
))}
</div>
);
}
// =============================================
// 4. ProfileContent - hiển thị nội dung theo tab
// =============================================
function ProfileContent({ activeTab, user }) {
// Dữ liệu mẫu cho từng tab (thực tế sẽ fetch từ API)
const mockData = {
posts: [
{
id: 1,
content: 'Hôm nay học React rất vui! 🚀',
likes: 42,
comments: 8,
},
{
id: 2,
content: 'Component composition là siêu quan trọng',
likes: 31,
comments: 5,
},
],
photos: [
{
id: 1,
url: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800',
},
{
id: 2,
url: 'https://images.unsplash.com/photo-1516321310764-9f3c9619d7d7?w=800',
},
],
videos: [
{ id: 1, title: 'Hướng dẫn useState trong 5 phút', duration: '4:52' },
{ id: 2, title: 'Tại sao nên học React năm 2026?', duration: '12:15' },
],
likes: [{ id: 1, content: 'Bài viết hay về Tailwind CSS' }],
};
const content = mockData[activeTab] || [];
if (content.length === 0) {
return (
<div className='empty-content'>
<p>Chưa có nội dung nào trong mục này.</p>
</div>
);
}
return (
<div className='profile-content'>
{activeTab === 'posts' && (
<div className='posts-list'>
{content.map((post) => (
<div
key={post.id}
className='post-item'
>
<p>{post.content}</p>
<div className='post-meta'>
<span>❤️ {post.likes}</span>
<span>•</span>
<span>💬 {post.comments}</span>
</div>
</div>
))}
</div>
)}
{activeTab === 'photos' && (
<div className='photos-grid'>
{content.map((photo) => (
<img
key={photo.id}
src={photo.url}
alt='User photo'
className='photo-item'
/>
))}
</div>
)}
{activeTab === 'videos' && (
<div className='videos-list'>
{content.map((video) => (
<div
key={video.id}
className='video-item'
>
<div className='video-thumbnail'>🎥</div>
<div>
<h4>{video.title}</h4>
<span>{video.duration}</span>
</div>
</div>
))}
</div>
)}
{activeTab === 'likes' && (
<div className='likes-list'>
{content.map((item) => (
<div
key={item.id}
className='like-item'
>
<p>{item.content}</p>
</div>
))}
</div>
)}
</div>
);
}
// =============================================
// Component chính: ProfilePage
// =============================================
function ProfilePage() {
const [activeTab, setActiveTab] = useState('posts');
// Dữ liệu user mẫu
const user = {
name: 'Tuân Dev',
username: 'tuan_dev',
avatarUrl:
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400',
bio: 'Full-stack developer | Yêu thích React & TypeScript | Đang học cách viết clean code mỗi ngày',
location: 'TP. Hồ Chí Minh',
joinDate: 'Tháng 3, 2024',
};
const stats = {
posts: 128,
followers: 2450,
following: 387,
};
return (
<div className='profile-page'>
<div className='container'>
<ProfileHeader user={user} />
<ProfileStats stats={stats} />
<ProfileTabs
activeTab={activeTab}
onTabChange={setActiveTab}
/>
<ProfileContent
activeTab={activeTab}
user={user}
/>
</div>
</div>
);
}
// =============================================
// App - Để chạy demo
// =============================================
function App() {
return (
<div className='app'>
<ProfilePage />
</div>
);
}
export default App;Ghi chú quan trọng:
- Sử dụng
useStateđể quản lý tab đang active (đây là phần nâng cao đầu tiên sử dụng state) - Truyền props một cách rõ ràng từ cha → con
- Component được chia nhỏ, dễ tái sử dụng và bảo trì
- Conditional rendering trong
ProfileContentdựa vàoactiveTab - Dữ liệu mock → sau này có thể thay bằng API call
Thử thách thêm (nếu bạn muốn luyện tập):
- Thêm nút Edit Profile chỉ hiển thị khi là profile của chính mình
- Thêm loading skeleton khi đang tải dữ liệu
- Làm cho tab Photos có lightbox khi click vào ảnh
- Thêm nút Follow/Unfollow thay đổi text và màu khi click
Bài 2: Data Visualization
/**
* Tạo Dashboard với charts (dùng div elements, không cần chart library):
* - BarChart component (vertical bars)
* - PieChart component (circular segments với CSS)
* - StatCard component (number + label + trend)
*
* Use:
* - Dynamic styling based on data
* - Calculations in JSX
* - Conditional colors
*/💡 Solution
// Bài tập về nhà NÂNG CAO - Bài 2: Data Visualization
// Tạo Dashboard đơn giản với:
// - StatCard (thẻ số liệu + trend)
// - BarChart (cột dọc dùng div)
// - PieChart (vòng tròn dùng CSS conic-gradient)
// Không dùng thư viện chart bên ngoài
import React from 'react';
// =============================================
// 1. StatCard - Thẻ hiển thị số liệu + xu hướng
// =============================================
function StatCard({ title, value, change, unit = '', trendUp = true }) {
const isPositive = change >= 0;
const trendColor = isPositive ? '#10b981' : '#ef4444';
const arrow = isPositive ? '↑' : '↓';
return (
<div className='stat-card'>
<h3 className='stat-title'>{title}</h3>
<div className='stat-value'>
{value.toLocaleString()}
{unit}
</div>
<div
className='stat-trend'
style={{ color: trendColor }}
>
{arrow} {Math.abs(change)}% so với tuần trước
</div>
</div>
);
}
// =============================================
// 2. BarChart - Biểu đồ cột dọc đơn giản dùng div
// =============================================
function BarChart({ data, title }) {
// Tìm giá trị lớn nhất để tính chiều cao tỉ lệ
const maxValue = Math.max(...data.map((item) => item.value));
return (
<div className='bar-chart-container'>
<h3 className='chart-title'>{title}</h3>
<div className='bars-wrapper'>
{data.map((item, index) => {
// Tính % chiều cao so với max
const heightPercent = (item.value / maxValue) * 100;
// Màu ngẫu nhiên hoặc theo thứ tự
const colors = [
'#3b82f6',
'#10b981',
'#f59e0b',
'#ef4444',
'#8b5cf6',
];
const barColor = colors[index % colors.length];
return (
<div
key={item.label}
className='bar-item'
>
<div
className='bar'
style={{
height: `${heightPercent}%`,
backgroundColor: barColor,
}}
>
<span className='bar-value'>{item.value}</span>
</div>
<span className='bar-label'>{item.label}</span>
</div>
);
})}
</div>
</div>
);
}
// =============================================
// 3. PieChart - Biểu đồ tròn dùng CSS conic-gradient
// =============================================
function PieChart({ data, title }) {
// Tính tổng để tính phần trăm
const total = data.reduce((sum, item) => sum + item.value, 0);
// Tạo gradient string
let cumulativePercent = 0;
const segments = data
.map((item) => {
const percent = (item.value / total) * 100;
const start = cumulativePercent;
cumulativePercent += percent;
return `${item.color} ${start}% ${cumulativePercent}%`;
})
.join(', ');
return (
<div className='pie-chart-container'>
<h3 className='chart-title'>{title}</h3>
<div className='pie-wrapper'>
<div
className='pie'
style={{
background: `conic-gradient(${segments})`,
}}
/>
<div className='pie-legend'>
{data.map((item, index) => (
<div
key={index}
className='legend-item'
>
<span
className='legend-color'
style={{ backgroundColor: item.color }}
/>
<span>
{item.label}: {item.value} (
{((item.value / total) * 100).toFixed(1)}%)
</span>
</div>
))}
</div>
</div>
</div>
);
}
// =============================================
// Dashboard - Ghép tất cả lại
// =============================================
function Dashboard() {
// Dữ liệu mẫu
const revenueData = [
{ label: 'Th 1', value: 4200000 },
{ label: 'Th 2', value: 3800000 },
{ label: 'Th 3', value: 6500000 },
{ label: 'Th 4', value: 5200000 },
{ label: 'Th 5', value: 7800000 },
{ label: 'Th 6', value: 9200000 },
];
const categoryData = [
{ label: 'Điện thoại', value: 45, color: '#3b82f6' },
{ label: 'Laptop', value: 28, color: '#10b981' },
{ label: 'Phụ kiện', value: 15, color: '#f59e0b' },
{ label: 'Khác', value: 12, color: '#ef4444' },
];
return (
<div className='dashboard'>
<header className='dashboard-header'>
<h1>Dashboard Doanh Thu 2026</h1>
<p>Cập nhật: {new Date().toLocaleDateString('vi-VN')}</p>
</header>
<div className='stats-grid'>
<StatCard
title='Doanh thu tháng này'
value={9200000}
change={18.4}
unit=' ₫'
/>
<StatCard
title='Đơn hàng'
value={384}
change={-5.2}
/>
<StatCard
title='Khách hàng mới'
value={127}
change={12.8}
/>
<StatCard
title='Tỷ lệ chuyển đổi'
value={4.8}
change={0.9}
unit='%'
/>
</div>
<div className='charts-grid'>
<BarChart
data={revenueData}
title='Doanh thu theo tháng (triệu ₫)'
/>
<PieChart
data={categoryData}
title='Phân bổ doanh thu theo danh mục'
/>
</div>
</div>
);
}
// =============================================
// App - Component gốc
// =============================================
function App() {
return (
<div className='app'>
<Dashboard />
</div>
);
}
export default App;Ghi chú quan trọng:
- Tất cả biểu đồ đều dùng HTML + CSS thuần (div, conic-gradient, percentage height)
- StatCard dùng conditional color dựa vào trend tăng/giảm
- BarChart tự động scale chiều cao cột dựa trên giá trị lớn nhất
- PieChart dùng
conic-gradient– rất mạnh mẽ và nhẹ - Dữ liệu được hard-code → sau này có thể thay bằng dữ liệu từ API
Thử thách mở rộng (nếu bạn muốn luyện thêm):
- Thêm tooltip khi hover vào cột/pie segment
- Làm animation khi load (height từ 0 → giá trị thật)
- Thêm nút chuyển đổi đơn vị (VND → USD)
- Tạo LineChart đơn giản bằng cách xếp nhiều div chồng nhau
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
React Official Docs - Hello World
- https://react.dev/learn
- Focus: First component, JSX basics
React Official Docs - JSX In Depth
- https://react.dev/learn/writing-markup-with-jsx
- Focus: JSX rules, expressions, differences from HTML
React Official Docs - JavaScript in JSX
- https://react.dev/learn/javascript-in-jsx-with-curly-braces
- Focus: Curly braces, expressions, objects
Đọc thêm
JSX Specification
- https://facebook.github.io/jsx/
- Deep dive into JSX syntax
React Without JSX
- https://react.dev/reference/react/createElement
- Understanding what JSX compiles to
Conditional Rendering Patterns
- https://react.dev/learn/conditional-rendering
- All ways to do conditional rendering
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền (từ Ngày 1-2)
- Destructuring → Dùng cho props
- Arrow functions → Define components
- Template literals → String trong JSX
- Array methods (map, filter) → Render lists
- Ternary operator → Conditional rendering
- Logical operators (&&, ||, ??) → Conditional rendering
Hướng tới (Ngày tiếp theo)
Ngày 4: Components & Props
- Sẽ học: Passing data giữa components
- Sẽ học: Props destructuring
- Sẽ học: Children prop
- Sẽ học: Component composition patterns
Ngày 5: Events & Conditional Rendering
- Sẽ học: Event handling
- Sẽ học: Conditional rendering deep dive
- Sẽ học: Event binding patterns
💡 SENIOR INSIGHTS
Cân Nhắc Production
- JSX vs createElement
// JSX (what we write)
<Button variant='primary'>Click</Button>;
// Compiled to (what runs)
React.createElement(Button, { variant: 'primary' }, 'Click');
// Performance: JSX compilation happens at BUILD time
// No runtime cost!- Component File Organization
src/
components/
Button/
Button.jsx # Component
Button.module.css # Styles (nếu dùng CSS Modules)
index.js # Re-export
Card/
Card.jsx
index.js
pages/
Dashboard.jsx
App.jsx- Performance Considerations
// ❌ Creating new objects in render
<div style={{ padding: '20px' }}> // New object every render!
// ✅ Define outside (at top of file) or use CSS
const styles = { padding: '20px' };
function MyComponent() {
return (
<div style={styles}>
Content goes here
</div>
);
}Câu Hỏi Phỏng Vấn
Junior Level:
Q: JSX là gì?
A: JSX là syntax extension cho JavaScript, cho phép viết HTML-like markup
trong JavaScript. Nó được compile thành React.createElement() calls.
Q: Khác nhau giữa class và className?
A: className là JSX equivalent của class attribute vì "class" là
reserved keyword trong JavaScript.
Q: Tại sao cần keys khi render lists?
A: Keys giúp React identify which items changed, added, or removed.
Điều này optimize re-rendering process.Mid Level:
Q: JSX được compile như thế nào?
A: JSX được Babel compile thành React.createElement() calls.
<div>Hello</div> → React.createElement('div', null, 'Hello')
Q: Khi nào nên dùng Fragment vs div wrapper?
A: Fragment (<></>) khi không cần extra DOM node.
Div khi cần wrapper cho styling hoặc event handling.
Q: Giải thích về JSX expression limitations
A: JSX chỉ accept expressions, không phải statements.
Có thể dùng: variables, function calls, ternary
Không dùng: if/else, for loops, switch (phải dùng IIFE)Senior Level:
Q: Performance implications của inline functions/objects trong JSX?
A: Inline functions/objects tạo new reference mỗi render.
Có thể cause unnecessary re-renders cho child components.
Solutions: useMemo, useCallback (sẽ học sau), hoặc define outside.
Q: JSX security considerations?
A: React tự động escapes values để prevent XSS attacks.
Tuy nhiên, dangerouslySetInnerHTML bypass protection này.
Chỉ dùng khi absolutely necessary và sanitize input.
Q: Babel JSX transform vs React 17+ JSX transform?
A: React 17+ introduced new JSX transform không require import React.
Old: import React from 'react'
New: Không cần (transform tự động import runtime)
Benefit: Smaller bundle size, better performanceWar Stories
Story 1: The Missing Key Bug
Vấn đề: E-commerce site có bug - khi thêm item vào cart,
wrong item được highlighted
Root cause: Dùng index làm key trong list
Fix: Chuyển sang dùng product.id làm key
Lesson: Index as key SEEMS to work until list reorders!Story 2: The Object Rendering Crash
Vấn đề: App crash với "Objects are not valid as React child"
Root cause: Accidentally rendering entire user object: {user}
Impact: Production down 15 minutes
Fix: {user.name} thay vì {user}
Lesson: TypeScript would have caught this at compile timeStory 3: The className Concatenation Bug
Vấn đề: Button styles không apply correctly
Root cause: 'button' + isActive ? 'active' : ''
// Operator precedence issue!
Fix: Use template literals or array approach
Lesson: Always test dynamic className logic thoroughly🎯 PREVIEW NGÀY MAI
Ngày 4: Components & Props
Bạn sẽ học:
- 📦 Component communication với Props
- 🔄 Props flow (parent → child)
- 🎨 Props destructuring patterns
- 👶 Children prop
- ⚡ Prop types và validation (concepts)
Chuẩn bị:
- [ ] Ôn lại destructuring (sẽ dùng rất nhiều!)
- [ ] Ôn lại JSX expressions
- [ ] Hiểu về function parameters
- [ ] Hoàn thành bài tập về nhà
Hẹn gặp bạn ngày mai khi học cách components "talk" to each other! 🚀
📊 Tổng kết Ngày 3:
- ✅ Đã học: React basics, JSX syntax, JSX vs HTML, Component structure
- ✅ Đã thực hành: 5 exercises từ basic đến component library
- ✅ Debug: 3 common JSX bugs
- 🎯 Sẵn sàng: Học Props và component communication!