📅 NGÀY 1: JSX & Rendering Basics
🎯 Mục tiêu hôm nay
- Hiểu JSX là gì và cách hoạt động
- Sử dụng JavaScript expressions trong JSX
- Làm việc với Fragments
- Tạo được UI components đơn giản
📚 PHẦN 1: THEORY (30-45 phút)
1.1. JSX là gì?
JSX (JavaScript XML) là cú pháp mở rộng của JavaScript cho phép viết HTML-like code trong JavaScript.
jsx
// JSX
const element = <h1>Hello, World!</h1>;
// Được transform thành JavaScript thuần:
const element = React.createElement('h1', null, 'Hello, World!');Key points:
- JSX không phải HTML, là JavaScript
- Được Babel transform thành
React.createElement() - Phải import React (React 17+ không cần nếu dùng new JSX transform)
1.2. JSX Syntax Basics
jsx
// 1. Single element
const element = <div>Hello</div>;
// 2. Attributes (camelCase)
const button = (
<button className='btn' onClick={handleClick}>
Click
</button>
);
// 3. Self-closing tags
const image = <img src='photo.jpg' alt='Photo' />;
// 4. Nested elements (phải có 1 root element)
const card = (
<div className='card'>
<h2>Title</h2>
<p>Content</p>
</div>
);Lưu ý quan trọng:
classNamethay vìclasshtmlForthay vìforonClickthay vìonclick(camelCase)- Style nhận object:
style={\{color: 'red', fontSize: '16px'\}}
1.3. Embedding JavaScript Expressions
Dùng {} để nhúng JavaScript:
jsx
const name = 'Alice';
const age = 25;
const greeting = (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
<p>Next year you'll be {age + 1}.</p>
<p>Random number: {Math.random()}</p>
</div>
);Có thể nhúng:
- Variables:
{name} - Expressions:
{age + 1},{name.toUpperCase()} - Function calls:
{getFullName()} - Ternary operators:
{isLoggedIn ? 'Welcome' : 'Please login'}
Không thể nhúng:
- Statements:
if,for,while - Object trực tiếp:
{user}❌ (phải{user.name}✅)
1.4. Fragments
Khi cần return nhiều elements mà không muốn thêm <div> wrapper:
jsx
// ❌ Error: JSX expressions must have one parent element
return (
<h1>Title</h1>
<p>Paragraph</p>
);
// ✅ Dùng div wrapper (thêm DOM node không cần thiết)
return (
<div>
<h1>Title</h1>
<p>Paragraph</p>
</div>
);
// ✅ Dùng Fragment - không tạo DOM node
return (
<React.Fragment>
<h1>Title</h1>
<p>Paragraph</p>
</React.Fragment>
);
// ✅ Short syntax (khuyên dùng)
return (
<>
<h1>Title</h1>
<p>Paragraph</p>
</>
);Khi nào cần <React.Fragment> đầy đủ? Khi cần key prop (trong list rendering):
jsx
{
items.map((item) => (
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
));
}💻 PHẦN 2: CODE DEMO (30-45 phút)
Tạo file App.jsx:
jsx
import React from 'react';
function App() {
// Data
const user = {
name: 'John Doe',
avatar: 'https://i.pravatar.cc/150?img=1',
age: 28,
isOnline: true,
};
const hobbies = ['Reading', 'Gaming', 'Coding'];
// Helper function
const formatDate = () => {
return new Date().toLocaleDateString('vi-VN');
};
return (
<>
{/* Header Section */}
<header
style={{
backgroundColor: '#282c34',
padding: '20px',
color: 'white',
}}
>
<h1>Welcome to JSX Demo</h1>
<p>Today is {formatDate()}</p>
</header>
{/* User Profile Card */}
<div className='profile-card'>
<img
src={user.avatar}
alt={`${user.name}'s avatar`}
style={{ borderRadius: '50%' }}
/>
<h2>{user.name}</h2>
<p>Age: {user.age}</p>
{/* Conditional rendering với ternary */}
<span
className={
user.isOnline ? 'status-online' : 'status-offline'
}
>
{user.isOnline ? '🟢 Online' : '🔴 Offline'}
</span>
{/* Hobbies list */}
<div>
<h3>Hobbies:</h3>
<ul>
{hobbies.map((hobby, index) => (
<li key={index}>{hobby}</li>
))}
</ul>
</div>
{/* Math expression */}
<p>
In 5 years, {user.name} will be {user.age + 5} years old
</p>
</div>
{/* Footer with Fragment */}
<>
<hr />
<footer>
<p>© 2024 JSX Demo App</p>
<p>Built with React {React.version}</p>
</footer>
</>
</>
);
}
export default App;Tạo file index.css để style:
css
.profile-card {
max-width: 400px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
text-align: center;
}
.status-online {
color: green;
font-weight: bold;
}
.status-offline {
color: red;
font-weight: bold;
}
ul {
list-style: none;
padding: 0;
}
li {
padding: 5px;
background: #f0f0f0;
margin: 5px 0;
border-radius: 4px;
}🔨 PHẦN 3: THỰC HÀNH (60-90 phút)
Exercise 1: Product Card Component
Tạo component hiển thị thông tin sản phẩm:
jsx
function ProductCard() {
const product = {
name: 'Wireless Headphones',
price: 99.99,
currency: 'USD',
inStock: true,
rating: 4.5,
reviews: 128,
discount: 20, // percent
image: 'https://via.placeholder.com/200',
};
// TODO:
// 1. Hiển thị ảnh sản phẩm
// 2. Hiển thị tên và giá (tính giá sau discount)
// 3. Hiển thị rating và số reviews
// 4. Hiển thị badge "In Stock" hoặc "Out of Stock" với màu khác nhau
// 5. Hiển thị % discount nếu có
return <div className='product-card'>{/* Your code here */}</div>;
}Gợi ý:
- Tính giá sau discount:
price * (1 - discount/100) - Dùng ternary cho stock status
- Format giá:
toFixed(2) - Dùng emoji cho rating: ⭐
💡 Nhấn để xem lời giải
jsx
import { Image } from '../../ui/Image/Image';
import './ProductCard.scss';
/**
* Rating Component
* - Render 5 sao với logic full/half/empty
* - Hiển thị tổng số reviews
*/
const Rating = ({ rating, reviews }: { rating: number, reviews: number }) => {
// Trả về emoji sao tương ứng với thang điểm rating
const getStar = (i: number) => {
if (rating >= i + 1) return '⭐'; // full star
if (rating >= i + 0.5) return '⯨'; // half star
return '☆'; // empty star
};
return (
<div className='product-card__rating'>
{/* Stars */}
<span className='product-card__rating-stars'>
{Array.from({ length: 5 }, (_, i) => (
<span key={i}>{getStar(i)}</span>
))}
</span>
{/* Reviews */}
<span className='product-card__rating-review'>({reviews})</span>
</div>
);
};
function ProductCard() {
// Mock product data — về sau dữ liệu sẽ đến từ API hoặc props
const product = {
name: 'Wireless Headphones',
price: 99.99,
currency: '$',
inStock: true,
rating: 4.5,
reviews: 128,
discount: 20,
image: 'https://placehold.co/400?text=Product',
};
/**
* Tính giá sau khi giảm
* - Tách logic ra trước return giúp JSX sạch và dễ đọc hơn
*/
const finalPrice = (product.price * (1 - product.discount / 100)).toFixed(
2
);
// Destructure để code ngắn gọn hơn
const { name, price, currency, rating, reviews, discount, image, inStock } =
product;
return (
<div className='product-card'>
{/* Product Image */}
<div className='product-card__image'>
<Image className='rounded-md' src={image} alt={name} />
</div>
{/* Discount badge (chỉ hiển thị khi discount > 0) */}
{discount > 0 && (
<div className='product-card__discount-badge'>-{discount}%</div>
)}
{/* Stock status */}
<div
className={`product-card__status product-card__status${
inStock ? '--instock' : '--outstock'
}`}
>
{inStock ? 'In Stock' : 'Out of Stock'}
</div>
{/* Product name */}
<h3 className='product-card__name'>{name}</h3>
{/* Rating + number of reviews */}
<Rating rating={rating} reviews={reviews} />
{/* Price section (price sau giảm + giá gốc nếu có discount) */}
<div className='product-card__price'>
{/* Final price */}
<span>
{currency} {finalPrice}
</span>
{/* Original price */}
{discount > 0 && (
<span className='product-card__price--old'>
{currency} {price.toFixed(2)}
</span>
)}
</div>
</div>
);
}
export default ProductCard;Exercise 2: Weather Widget
jsx
function WeatherWidget() {
const weather = {
city: 'Hanoi',
temperature: 28,
condition: 'Sunny', // "Sunny", "Rainy", "Cloudy"
humidity: 65,
wind: 12, // km/h
};
// TODO:
// 1. Hiển thị icon dựa vào condition (☀️ Sunny, 🌧️ Rainy, ☁️ Cloudy)
// 2. Hiển thị nhiệt độ với unit °C
// 3. Hiển thị thành phố
// 4. Hiển thị humidity và wind speed
// 5. Background color khác nhau cho mỗi condition
return <div className='weather-widget'>{/* Your code here */}</div>;
}💡 Nhấn để xem lời giải
jsx
import './WeatherWidget.scss';
interface Weather {
city: string;
temperature: number;
condition: 'Sunny' | 'Rainy' | 'Cloudy';
humidity: number;
wind: number;
}
// Default fallback weather nếu không truyền props
const defaultWeather: Weather = {
city: 'Hanoi',
temperature: 28,
condition: 'Sunny',
humidity: 65,
wind: 12,
};
function WeatherWidget({ weather = defaultWeather }: { weather?: Weather }) {
/**
* Icon cho từng trạng thái thời tiết
* - Tách object ra ngoài JSX → code gọn & dễ maintain
*/
const icons = {
Sunny: '☀️',
Rainy: '🌧️',
Cloudy: '☁️',
} as const;
// Destructure để JSX sạch hơn
const { city, temperature, condition, humidity, wind } = weather;
return (
/**
* Root container của widget
* - BEM: weather-widget--sunny / --rainy / --cloudy
* - Dùng condition để đổi background trong SCSS
*/
<div className={`weather-widget weather-widget--${condition.toLowerCase()}`}>
{/* Weather icon */}
<div className="weather-widget__icon">
{icons[condition]}
</div>
{/* Temperature with °C unit */}
<div className="weather-widget__temperature">
{temperature}°C
</div>
{/* City name */}
<div className="weather-widget__city">
{city}
</div>
{/* Humidity + Wind group */}
<div className="weather-widget__stats">
{/* Humidity */}
<div className="weather-widget__stats-item">
💦 {humidity}%
</div>
{/* Wind speed */}
<div className="weather-widget__stats-item">
༄ {wind} km/h
</div>
</div>
</div>
);
}
export default WeatherWidget;Exercise 3: User Stats Dashboard
jsx
function UserDashboard() {
const stats = {
posts: 145,
followers: 2340,
following: 890,
likes: 5678
};
const recentActivities = [
"Posted a new photo",
"Liked John's post",
"Followed Jane Smith"
];
// TODO:
// 1. Hiển thị 4 stats trong grid layout
// 2. Format numbers với commas (2,340)
// 3. Hiển thị danh sách activities
// 4. Thêm icons cho mỗi stat
// 5. Dùng Fragment hợp lý
return (
// Your code here
);
}💡 Nhấn để xem lời giải
jsx
import "./UserDashboard.scss";
type Stats = {
posts: number;
followers: number;
following: number;
likes: number;
};
export default function UserDashboard() {
const stats: Stats = {
posts: 145,
followers: 2340,
following: 890,
likes: 5678,
};
type StatKey = keyof typeof stats;
const recentActivities = [
"Posted a new photo",
"Liked John's post",
"Followed Jane Smith",
];
const statIcons: Record<StatKey, string> = {
posts: "📝",
followers: "👥",
following: "➡️",
likes: "❤️",
};
// TODO:
// 1. Hiển thị 4 stats trong grid layout
// 2. Format numbers với commas (2,340)
// 3. Hiển thị danh sách activities
// 4. Thêm icons cho mỗi stat
// 5. Dùng Fragment hợp lý
return (
<div className="user-dashboard">
{/* GRID STATS SECTION */}
<div className="user-dashboard__stats">
{Object.entries(stats).map(([key, value]) => {
const statKey = key as StatKey;
const label = key.charAt(0).toUpperCase() + key.slice(1);
return (
<div key={key} className="user-dashboard__stats-item">
<h3>
{statIcons[statKey]} {label} {/* vd: 📝Post */}
</h3>
<span>{Intl.NumberFormat().format(value)}</span>
</div>
)
})}
</div>
{/* ACTIVITIES SECTION */}
{recentActivities.length > 0 ? (
<ul className="user-dashboard__activities">
{recentActivities.map((activity, index) => (
<li key={index} className="user-dashboard__activities--item">
{activity}
</li>
))}
</ul>
) : (
<p className="user-dashboard__activities--no-data">No data</p>
)}
</div>
);
}Exercise 4: Dynamic Greeting (Challenge)
jsx
function DynamicGreeting() {
const currentHour = new Date().getHours();
const userName = 'Alex';
// TODO:
// 1. Hiển thị greeting dựa vào thời gian:
// - 5-11: Good Morning 🌅
// - 12-17: Good Afternoon ☀️
// - 18-21: Good Evening 🌆
// - 22-4: Good Night 🌙
// 2. Hiển thị tên user
// 3. Hiển thị giờ hiện tại
// 4. Background color khác nhau cho mỗi thời điểm
// 5. Bonus: Thêm motivational quote
return <div className='greeting'>{/* Your code here */}</div>;
}💡 Nhấn để xem lời giải
jsx
import './DynamicGreeting.scss';
// 📌 as const giúp QUOTES_ICONS trở thành literal type,
// đảm bảo các key ('morning' | 'afternoon' | ...) được giữ nguyên
// và TypeScript suy luận chính xác hơn, tăng độ an toàn type.
const QUOTES_ICONS = {
morning: { icon: '🌅', quote: 'Start your day with a grateful heart.' },
afternoon: { icon: '☀️', quote: 'Energy flows where focus goes' },
evening: {
icon: '🌆',
quote: 'Slow down and appreciate what you achieved today',
},
night: { icon: '🌙', quote: 'Sleep well — a new beginning awaits' },
} as const;
// 🎯 Greeting là union type: 'morning' | 'afternoon' | 'evening' | 'night'
type Greeting = keyof typeof QUOTES_ICONS;
// 📌 Hàm xác định greeting từ giờ hiện tại
const greeting = (hour: number): Greeting => {
if (hour >= 5 && hour <= 11) return 'morning';
if (hour >= 12 && hour <= 17) return 'afternoon';
if (hour >= 18 && hour <= 21) return 'evening';
return 'night'; // 22-4
};
// 📌 Capitalize string (utility nhỏ, dùng nhiều lần thì nên tách ra utils)
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
function DynamicGreeting() {
const current = new Date();
const userName = 'Alex'; // Có thể fetch từ API hoặc context
const time = greeting(current.getHours());
const { icon, quote } = QUOTES_ICONS[time];
/*
TODO:
1. Hiển thị greeting theo thời gian (Good Morning / Afternoon / Evening / Night)
2. Hiển thị tên user
3. Hiển thị thời gian hiện tại
4. Background khác nhau theo thời điểm (xử lý trong SCSS theo class .greeting--{time})
5. Bonus: Hiển thị câu motivational quote
*/
return (
<div className={`greeting greeting--${time}`}>
{/* Title greeting */}
<h1 className='greeting__time'>
{`Good ${capitalize(time)} ${icon} `}
</h1>
{/* Username */}
<p className='greeting__name'>{userName}</p>
{/* Current time */}
<p className='greeting__current-time'>
Current Time : {current.toLocaleString()}
</p>
{/* Motivational quote */}
<p className='greeting__motivational-quote'>{quote}</p>
</div>
);
}
export default DynamicGreeting;✅ PHẦN 4: REVIEW & CHECKLIST (15-30 phút)
Kiến thức cần nắm vững:
- [ ] JSX là gì? Transform thành gì?
- [ ] Cách nhúng JavaScript expressions với
{} - [ ] Phân biệt được expressions vs statements
- [ ] Sử dụng camelCase cho attributes
- [ ]
classNamethay vìclass - [ ] Style nhận object với camelCase properties
- [ ] Khi nào dùng
<></>vs<React.Fragment> - [ ] Mọi JSX expression phải có 1 root element
Common Mistakes:
jsx
// ❌ Không có root element
return (
<h1>Title</h1>
<p>Text</p>
);
// ❌ Dùng class thay vì className
<div class="container">
// ❌ Nhúng object trực tiếp
<div>{user}</div> // Objects are not valid as a React child
// ❌ Style string
<div style="color: red"> // Phải dùng object
// ✅ Đúng
return (
<>
<h1>Title</h1>
<p>Text</p>
</>
);
<div className="container">
<div>{user.name}</div>
<div style={{color: 'red'}}>🎯 HOMEWORK
- Tạo Personal Bio Card với:
jsx
function PersonalBioCard() {
const user = {
avatar: "https://i.pravatar.cc/150?img=12",
name: "Le Tuan",
title: "Front-end Engineer",
company: "VNG",
socials: {
github: "github.com/letuan",
linkedin: "linkedin.com/in/letuan",
twitter: "twitter.com/letuan"
},
skills: ["React", "TypeScript", "Node.js", "UI/UX"],
bio: "A passionate front-end engineer who loves crafting beautiful UI and clean code."
};
// TODO:
// 1. Render avatar + name + title + company
// 2. Render social links dưới dạng list
// 3. Render skills list
// 4. Render bio paragraph
// 5. Dùng Fragment để wrap component
// 6. Format title như: "Front-end Engineer @ VNG"
return (
// Your code here
);
}💡 Nhấn để xem lời giải
jsx
import './PersonalBioCard.scss';
type Socials = {
github: string,
linkedin: string,
twitter: string,
};
type User = {
avatar: string,
name: string,
title: string,
company: string,
socials: Socials,
skills: string[],
bio: string,
};
// 📌 Tách constant user ra ngoài component để tránh recreate mỗi render
const user: User = {
avatar: 'https://i.pravatar.cc/150?img=12',
name: 'Le Tuan',
title: 'Front-end Engineer',
company: 'VNG',
socials: {
github: 'github.com/letuan',
linkedin: 'linkedin.com/in/letuan',
twitter: 'twitter.com/letuan',
},
skills: ['React', 'TypeScript', 'Node.js', 'UI/UX'],
bio: 'A passionate front-end engineer who loves crafting beautiful UI and clean code.',
};
// Utility: Capitalize social name
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
function PersonalBioCard() {
const { avatar, name, title, company, socials, skills, bio } = user;
return (
<>
<div className='bio-card'>
{/* Header */}
<div className='bio-card__header'>
<img className='bio-card__avatar' src={avatar} alt={name} />
<div className='bio-card__info'>
<h2 className='bio-card__name'>{name}</h2>
<p className='bio-card__title'>
{title} @ {company}
</p>
</div>
</div>
{/* Skills */}
<ul className='bio-card__skills'>
{skills.map((skill) => (
<li key={skill} className='bio-card__skill'>
{skill}
</li>
))}
</ul>
{/* Socials */}
<ul className='bio-card__socials'>
{Object.entries(socials).map(([network, url]) => (
<li key={network} className='bio-card__social'>
<a
href={`https://${url}`}
target='_blank'
rel='noopener noreferrer'
className='bio-card__social-link'
>
{capitalize(network)}
</a>
</li>
))}
</ul>
{/* Bio */}
<p className='bio-card__bio'>{bio}</p>
</div>
</>
);
}
export default PersonalBioCard;- Tạo Restaurant Menu Component:
jsx
function RestaurantMenu() {
const restaurant = {
logo: "https://cdn-icons-png.flaticon.com/512/3075/3075977.png",
name: "Green Leaf Restaurant",
};
const dishes = [
{ name: "Margherita Pizza", price: 12.5, description: "Classic cheese pizza", isVegetarian: true },
{ name: "Beef Steak", price: 25, description: "Grilled premium beef", isVegetarian: false },
{ name: "Pasta Alfredo", price: 14, description: "Creamy white sauce pasta", isVegetarian: true },
];
// TODO:
// 1. Render logo + restaurant name
// 2. Render list món ăn
// 3. Nếu isVegetarian → thêm icon 🌱
// 4. Format price: $12.50
// 5. Tính tổng giá = sum(price)
// 6. Dùng Fragment hợp lý
return (
// Your code here
);
}💡 Nhấn để xem lời giải
jsx
import './RestaurantMenu.scss';
function RestaurantMenu() {
const restaurant = {
logo: 'https://cdn-icons-png.flaticon.com/512/3075/3075977.png',
name: 'Green Leaf Restaurant',
};
const dishes = [
{
name: 'Margherita Pizza',
price: 12.5,
description: 'Classic cheese pizza',
isVegetarian: true,
},
{
name: 'Beef Steak',
price: 25,
description: 'Grilled premium beef',
isVegetarian: false,
},
{
name: 'Pasta Alfredo',
price: 14,
description: 'Creamy white sauce pasta',
isVegetarian: true,
},
];
const total = dishes.reduce((acc, item) => acc + item.price, 0);
// TODO:
// 1. Render logo + restaurant name
// 2. Render list món ăn
// 3. Nếu isVegetarian → thêm icon 🌱
// 4. Format price: $12.50
// 5. Tính tổng giá = sum(price)
// 6. Dùng Fragment hợp lý
return (
<div className='restaurant-menu'>
<div className='restaurant-menu__header'>
<img
className='restaurant-menu__logo'
alt='Logo'
src={restaurant.logo}
/>
<h2 className='restaurant-menu__name'>{restaurant.name}</h2>
</div>
<ul className='restaurant-menu__list'>
{dishes.map((dish, key) => (
<li key={key} className='restaurant-menu__item'>
<p className='restaurant-menu__item-name'>
{dish.name}
{dish.isVegetarian && '🌱'}
</p>
<p className='restaurant-menu__item-price'>
${dish.price.toFixed(2)}
</p>
<p className='restaurant-menu__item-description'>
{dish.description}
</p>
</li>
))}
</ul>
<div className='restaurant-menu__total'>
Total: ${total.toFixed(2)}
</div>
</div>
);
}
export default RestaurantMenu;📝 NOTES
Ghi lại những điều bạn học được hôm nay:
- Điều gì khó nhất?
- Điều gì bất ngờ nhất?
- Questions để research thêm?
🚀 Ngày mai: Conditional & List Rendering - học cách render động và làm việc với arrays!