Skip to content

📅 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:

  • className thay vì class
  • htmlFor thay vì for
  • onClick thay 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>&copy; 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
  • [ ] className thay 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

  1. 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;
  1. 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;
  1. Đọc thêm:

📝 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!

Personal tech knowledge base