📙 Phần 3 · Xây Dựng Web · Chương 6

Frontend với AI

Xây dựng giao diện web hiện đại và đẹp với HTML/CSS, JavaScript, React và Tailwind CSS — tận dụng tối đa AI để code nhanh hơn 5x.

5 giờ học
📝 4 bài học
🎯 2 dự án mini
📊 Mức độ: Trung cấp

🎯 Mục tiêu học tập

  • Dùng AI để tạo layout HTML/CSS nhanh chóng và responsive
  • Viết JavaScript tương tác với DOM và Fetch API hiệu quả
  • Xây dựng React component với Hooks sử dụng AI-assisted workflow
  • Kết hợp Tailwind CSS và AI để thiết kế UI cực nhanh
1

Bài 6.1 — HTML/CSS Hiện Đại với AI

CSS Custom Properties
Biến CSS định nghĩa với prefix --, dùng qua var(--name). Nền tảng của Design System: thay đổi 1 chỗ, ảnh hưởng toàn bộ app.
React Component
Hàm trả về JSX. Có 2 loại: Function component (hiện đại, dùng hooks) và Class component (cũ, légaxy). Luôn dùng function component.
Props vs State
Props: dữ liệu truyền từ cha xuống con, read-only. State: dữ liệu nội bộ của component, mật khi thay đổi sẽ re-render.
Virtual DOM
Bản sao DOM của React trong bộ nhớ. Khi state thay đổi, React diff v-DOM với DOM thật, chỉ update những gì thực sự đổi — hiệu năng tốt hơn.
Tailwind CSS
Utility-first CSS framework. Không viết CSS file riêng — dùng class trực tiếp: flex items-center gap-4 p-4 bg-gray-900 rounded-xl.
Reconciliation
Quá trình React so sánh Virtual DOM cũ và mới sau mỗi re-render. Thuật toán diff O(n) của React giúp update UI hiệu quả.

AI đặc biệt hiệu quả với HTML/CSS vì đây là code có pattern rất rõ ràng. Chiến lược tốt nhất:

1. Describe → Generate

Mô tả UI bạn muốn bằng tiếng tự nhiên, AI tạo ra HTML/CSS hoàn chỉnh. Tiết kiệm 80% thời gian viết boilerplate.

2. Sketch → Code

Vẽ wireframe tay hoặc mô tả layout dạng ASCII, paste vào Copilot Chat để generate code.

3. Existing → Improve

Paste HTML code cũ, nhờ AI: "Làm responsive cho mobile", "Cải thiện accessibility", "Thêm dark mode".

Ví dụ: Prompt tạo Landing Page

text — Prompt cho Copilot Chat
Tạo landing page cho một SaaS tool tên "DevFlow".
Layout:
- Hero section: headline lớn, subtitle, 2 CTA buttons (primary + secondary)
- Features section: 3 cards grid (icon, title, description)  
- Pricing section: 3 tiers (Free, Pro $19/tháng, Enterprise custom)
- Footer: logo, 3 cột links, copyright

Yêu cầu:
- HTML5 semantic (header, main, section, footer)
- CSS thuần túy, không framework
- Responsive (mobile-first) với breakpoint 768px
- Color scheme: dark background (#0a0a0a), primary accent (#6C63FF)
- Font: Inter từ Google Fonts
- Smooth scroll, hover effects trên buttons và cards

Flexbox và Grid — Nền tảng layout hiện đại

css
/* === FLEXBOX — layout 1 chiều === */
.navbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 2rem;
  gap: 1rem;
}

/* Responsive: stack vertically on mobile */
@media (max-width: 768px) {
  .navbar { flex-direction: column; }
}

/* === GRID — layout 2 chiều === */
.features-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
  padding: 2rem 0;
}

/* Pricing grid: 3 columns, middle highlighted */
.pricing-grid {
  display: grid;
  grid-template-columns: 1fr 1.1fr 1fr; /* Middle slightly larger */
  gap: 1rem;
  align-items: start;
}

/* CSS Custom Properties (variables) */
:root {
  --color-primary: #6C63FF;
  --color-bg: #0a0a0a;
  --color-text: #e8eaf6;
  --radius: 12px;
  --shadow: 0 4px 24px rgba(0,0,0,0.3);
  --transition: all 0.2s ease;
}

/* Component: Card */
.card {
  background: #141828;
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius);
  padding: 1.5rem;
  transition: var(--transition);
}

.card:hover {
  border-color: var(--color-primary);
  transform: translateY(-4px);
  box-shadow: var(--shadow);
}

Live Server — Xem thay đổi ngay lập tức

💡
Dùng Live Server extension

Click chuột phải vào file HTML → "Open with Live Server". Browser sẽ tự reload khi bạn lưu file (Ctrl+S). Cực kỳ tiện khi làm việc với AI — generate code, save, thấy kết quả ngay.


2

Bài 6.2 — JavaScript Tương Tác

DOM Manipulation Patterns

javascript
// === CHỌN ELEMENTS ===
const btn    = document.querySelector('#my-button');       // 1 element
const items  = document.querySelectorAll('.item');         // NodeList
const input  = document.getElementById('search-input');   // By ID

// === THÊM/XÓA CLASS ===
btn.classList.add('active');
btn.classList.remove('active');
btn.classList.toggle('active');      // Add nếu không có, remove nếu có

// === TẠO ELEMENT ĐỘNG ===
function createCard(item) {
  const card = document.createElement('div');
  card.className = 'card';
  card.innerHTML = `
    

${item.title}

${item.description}

`; return card; } // === EVENT DELEGATION (hiệu quả hơn addEventListener trên nhiều items) === document.querySelector('#list').addEventListener('click', (e) => { const deleteBtn = e.target.closest('.btn-delete'); if (deleteBtn) { const id = deleteBtn.dataset.id; deleteItem(id); } }); // === FETCH API — gọi API === async function fetchTodos() { try { const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=10'); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const todos = await response.json(); renderTodos(todos); } catch (error) { showError('Không thể tải dữ liệu: ' + error.message); } } function renderTodos(todos) { const container = document.querySelector('#todos-container'); container.innerHTML = todos.map(todo => `
${todo.title}
`).join(''); }

3

Bài 6.3 — React với AI

Setup React với Vite (nhanh nhất hiện tại)

bash
# Tạo React app với Vite
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
# → Mở browser: http://localhost:5173

Component Pattern với AI

Khi dùng AI để tạo React components, luôn cung cấp:

text — Prompt mẫu cho React component
Tôi đang dùng React 18 với Vite. Viết component ProductCard:
Props: { id, name, price, image, rating, onAddToCart }
- Hiển thị ảnh sản phẩm, tên, giá (format VND), rating (stars)
- Button "Thêm vào giỏ" gọi onAddToCart(id)
- Loading state khi đang xử lý
- Dùng CSS modules (ProductCard.module.css)
- PropTypes validation
- Responsive: full width trên mobile, 1/3 trên desktop
jsx — ProductCard.jsx (AI generated + reviewed)
import { useState } from 'react';
import PropTypes from 'prop-types';
import styles from './ProductCard.module.css';

function StarRating({ rating }) {
  return (
    
{[1, 2, 3, 4, 5].map((star) => ( ))} ({rating.toFixed(1)})
); } function ProductCard({ id, name, price, image, rating, onAddToCart }) { const [isLoading, setIsLoading] = useState(false); const [added, setAdded] = useState(false); const handleAddToCart = async () => { setIsLoading(true); try { await onAddToCart(id); setAdded(true); setTimeout(() => setAdded(false), 2000); } finally { setIsLoading(false); } }; const formattedPrice = new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND', }).format(price); return (
{name}

{name}

{formattedPrice}

); } ProductCard.propTypes = { id: PropTypes.number.isRequired, name: PropTypes.string.isRequired, price: PropTypes.number.isRequired, image: PropTypes.string.isRequired, rating: PropTypes.number.isRequired, onAddToCart: PropTypes.func.isRequired, }; export default ProductCard;

React Hooks thường dùng

HookDùng khi nàoVí dụLưu ý
useStateState đơn giản trong componentconst [count, setCount] = useState(0)Không mutate state trực tiếp
useEffectSide effects: fetch, subscription, timerFetch data khi component mountPhải có cleanup nếu có subscription
useCallbackStable function reference cho propsHandler truyền xuống child componentChỉ dùng khi child dùng React.memo
useMemoCache tính toán nặngFilter/sort danh sách lớnĐừng lạm dụng — overhead nhỏ cũng có
useRefDOM reference hoặc mutable valueinputRef.current.focus()Không trigger re-render khi thay đổi
useContextChia sẻ state toàn cụcTheme, Auth, LanguageRe-render mọi consumer khi đổi
useReducerState phức tạp, nhiều actionsShopping cart, form phức tạpThay useState khi logic > 3 cases
Custom HookTái sử dụng logic có stateuseFetch(url), useLocalStorage(key)Luôn bắt đầu bằng use
import { useState, useEffect, useCallback, useMemo } from 'react';

// useState — quản lý state đơn giản
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);

// useEffect — side effects (fetch, subscriptions, timers)
useEffect(() => {
  // Chạy khi component mount
  fetchUser(userId).then(setUser);
  
  return () => {
    // Cleanup khi unmount
  };
}, [userId]); // Dependency array — chạy lại khi userId thay đổi

// useCallback — memoize function (tránh re-render con)
const handleSearch = useCallback((query) => {
  setSearchQuery(query);
}, []); // Empty deps = function không đổi

// useMemo — memoize tính toán nặng
const filteredItems = useMemo(
  () => items.filter(item => item.name.includes(searchQuery)),
  [items, searchQuery]
);

// Custom Hook — tái sử dụng logic
function useFetch(url) {
  const [data, setData]     = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError]   = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    
    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') setError(err.message);
      })
      .finally(() => setLoading(false));
    
    return () => controller.abort(); // Cleanup
  }, [url]);

  return { data, loading, error };
}

Quy Trình Phát Triển Component Chuẩn

1
Define Props Interface

Xác định component nhận props gì: kiểu dữ liệu, required/optional, default values. Dùng TypeScript interface hoặc PropTypes.

2
Sketch UI States

Liệt kê tất cả states: loading, error, empty, data loaded, user interaction. Mỗi state phải có UI khác nhau.

3
Tách Logic vào Custom Hook

Business logic (fetch, filter, compute) vào hook riêng. Component chỉ chứa JSX + UI handlers. Dễ test, dễ reuse.

4
Prompt Copilot với đầy đủ context

Cung cấp: tên component, props interface, behavior mong muốn, edge cases cần handle. Output tốt ngay lần đầu.

5
Review + Test

Đọc từng dòng. Test thủ công tất cả states. Kiểm tra re-render không cần thiết bằng React DevTools Profiler.

6
Document & Export

Thêm JSDoc comment cho props. Export từ index.ts barrel file. Các team member import dễ dàng.


4

Bài 6.4 — Tailwind CSS + AI = Siêu Tốc

Tailwind CSS + AI là combo cực kỳ mạnh. AI biết hầu hết Tailwind classes — bạn chỉ cần mô tả UI bằng tiếng tự nhiên.

Setup Tailwind với Vite

bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# Thêm vào index.css:
# @tailwind base;
# @tailwind components;
# @tailwind utilities;

AI + Tailwind Workflow

text — Prompt mẫu
Dùng Tailwind CSS (v3) viết component Notification Toast:
- 4 variants: success (green), error (red), warning (yellow), info (blue)
- Mỗi toast có: icon, title, message, close button
- Animation: slide in từ phải, tự đóng sau 5 giây
- Stack nhiều toasts theo chiều dọc, góc dưới phải màn hình
- React component, nhận prop: { type, title, message, onClose }
jsx — Toast.jsx với Tailwind
import { useEffect } from 'react';

const VARIANTS = {
  success: { bg: 'bg-green-50',  border: 'border-green-200', icon: '✅', title: 'text-green-800', msg: 'text-green-700' },
  error:   { bg: 'bg-red-50',    border: 'border-red-200',   icon: '❌', title: 'text-red-800',   msg: 'text-red-700'   },
  warning: { bg: 'bg-yellow-50', border: 'border-yellow-200',icon: '⚠️', title: 'text-yellow-800',msg: 'text-yellow-700'},
  info:    { bg: 'bg-blue-50',   border: 'border-blue-200',  icon: 'ℹ️', title: 'text-blue-800',  msg: 'text-blue-700'  },
};

export function Toast({ type = 'info', title, message, onClose }) {
  const v = VARIANTS[type];

  useEffect(() => {
    const timer = setTimeout(onClose, 5000);
    return () => clearTimeout(timer);
  }, [onClose]);

  return (
    
{v.icon}

{title}

{message &&

{message}

}
); } // Container đặt ở góc phải dưới export function ToastContainer({ toasts, removeToast }) { return (
{toasts.map((toast) => ( removeToast(toast.id)} /> ))}
); }
Pro tip: Tailwind IntelliSense

Cài extension Tailwind CSS IntelliSense trong VS Code. Khi gõ className, autocomplete hiện danh sách classes kèm preview. Copilot + IntelliSense = không cần nhớ bất kỳ class nào!


5

Bài 6.5 — Dự Án Thực Hành: Xây Dựng React Task Manager App

Chúng ta sẽ xây dựng một ứng dụng Task Manager bằng React + Tailwind CSS — có thêm/xóa/sửa task, lọc theo trạng thái, lưu localStorage, và responsive layout. Toàn bộ UI được viết với sự hỗ trợ của Copilot.

Bước 1 — Tạo Dự Án React Với Vite

bash — Tạo project Vite + React
# Tạo project mới với Vite (nhanh hơn Create React App 10x)
npm create vite@latest task-manager -- --template react
cd task-manager

# Cài dependencies mặc định
npm install

# Cài Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# Cài thư viện bổ sung
npm install lucide-react uuid

# Chạy development server
npm run dev
# Mở trình duyệt tại: http://localhost:5173
javascript — tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
}
css — src/index.css (thay thế toàn bộ)
@tailwind base;
@tailwind components;
@tailwind utilities;

Bước 2 — Cấu Trúc Components

bash — Tạo component files
mkdir src/components src/hooks
# Windows:
ni src/components/TaskForm.jsx, src/components/TaskItem.jsx, src/components/TaskList.jsx, src/components/FilterBar.jsx, src/hooks/useTasks.js
# macOS/Linux:
touch src/components/TaskForm.jsx src/components/TaskItem.jsx src/components/TaskList.jsx src/components/FilterBar.jsx src/hooks/useTasks.js

Bước 3 — Custom Hook useTasks.js

javascript — src/hooks/useTasks.js
// Prompt Copilot: "Viết custom hook useTasks với localStorage persistence, hỗ trợ add/toggle/delete/edit"
import { useState, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';

const STORAGE_KEY = 'task-manager-tasks';

export function useTasks() {
  const [tasks, setTasks] = useState(() => {
    try {
      const saved = localStorage.getItem(STORAGE_KEY);
      return saved ? JSON.parse(saved) : [];
    } catch {
      return [];
    }
  });

  // Tự động lưu localStorage mỗi khi tasks thay đổi
  useEffect(() => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
  }, [tasks]);

  const addTask = (title, priority = 'medium') => {
    if (!title.trim()) return;
    setTasks(prev => [...prev, {
      id: uuidv4(),
      title: title.trim(),
      priority,          // 'low' | 'medium' | 'high'
      completed: false,
      createdAt: new Date().toISOString(),
    }]);
  };

  const toggleTask = (id) => {
    setTasks(prev => prev.map(t =>
      t.id === id ? { ...t, completed: !t.completed } : t
    ));
  };

  const deleteTask = (id) => {
    setTasks(prev => prev.filter(t => t.id !== id));
  };

  const editTask = (id, newTitle) => {
    if (!newTitle.trim()) return;
    setTasks(prev => prev.map(t =>
      t.id === id ? { ...t, title: newTitle.trim() } : t
    ));
  };

  const clearCompleted = () => {
    setTasks(prev => prev.filter(t => !t.completed));
  };

  return { tasks, addTask, toggleTask, deleteTask, editTask, clearCompleted };
}

Bước 4 — Components

jsx — src/components/TaskForm.jsx
import { useState } from 'react';
import { Plus } from 'lucide-react';

const PRIORITY_OPTIONS = [
  { value: 'low', label: 'Thấp', color: 'text-green-400' },
  { value: 'medium', label: 'Trung bình', color: 'text-yellow-400' },
  { value: 'high', label: 'Cao', color: 'text-red-400' },
];

export function TaskForm({ onAdd }) {
  const [title, setTitle] = useState('');
  const [priority, setPriority] = useState('medium');

  const handleSubmit = (e) => {
    e.preventDefault();
    onAdd(title, priority);
    setTitle('');
  };

  return (
    <form onSubmit={handleSubmit} className="flex gap-2 mb-6">
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
        placeholder="Thêm công việc mới..."
        className="flex-1 px-4 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-indigo-500"
      />
      <select
        value={priority}
        onChange={e => setPriority(e.target.value)}
        className="px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none"
      >
        {PRIORITY_OPTIONS.map(opt => (
          <option key={opt.value} value={opt.value}>{opt.label}</option>
        ))}
      </select>
      <button
        type="submit"
        disabled={!title.trim()}
        className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 text-white rounded-lg flex items-center gap-1 transition-colors"
      >
        <Plus size={18} /> Thêm
      </button>
    </form>
  );
}
jsx — src/components/TaskItem.jsx
import { useState } from 'react';
import { Trash2, Pencil, Check, X } from 'lucide-react';

const PRIORITY_COLORS = {
  low: 'border-green-500/30 bg-green-500/5',
  medium: 'border-yellow-500/30 bg-yellow-500/5',
  high: 'border-red-500/30 bg-red-500/5',
};

const PRIORITY_BADGES = {
  low: 'bg-green-500/20 text-green-400',
  medium: 'bg-yellow-500/20 text-yellow-400',
  high: 'bg-red-500/20 text-red-400',
};

export function TaskItem({ task, onToggle, onDelete, onEdit }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editText, setEditText] = useState(task.title);

  const handleSave = () => {
    onEdit(task.id, editText);
    setIsEditing(false);
  };

  return (
    <div className={`flex items-center gap-3 p-3 rounded-lg border ${PRIORITY_COLORS[task.priority]} mb-2`}>
      <button onClick={() => onToggle(task.id)}
        className={`w-5 h-5 rounded-full border-2 flex-shrink-0 flex items-center justify-center transition-colors ${task.completed ? 'bg-indigo-600 border-indigo-600' : 'border-gray-500 hover:border-indigo-400'}`}>
        {task.completed && <Check size={12} className="text-white" />}
      </button>

      {isEditing ? (
        <>
          <input value={editText} onChange={e => setEditText(e.target.value)}
            onKeyDown={e => e.key === 'Enter' && handleSave()}
            className="flex-1 bg-gray-700 px-2 py-1 rounded text-white text-sm outline-none" autoFocus />
          <button onClick={handleSave} className="text-green-400 hover:text-green-300"><Check size={16} /></button>
          <button onClick={() => setIsEditing(false)} className="text-red-400 hover:text-red-300"><X size={16} /></button>
        </>
      ) : (
        <>
          <span className={`flex-1 text-sm ${task.completed ? 'line-through text-gray-500' : 'text-gray-100'}`}>
            {task.title}
          </span>
          <span className={`text-xs px-2 py-0.5 rounded-full ${PRIORITY_BADGES[task.priority]}`}>
            {task.priority === 'high' ? 'Cao' : task.priority === 'medium' ? 'TB' : 'Thấp'}
          </span>
          <button onClick={() => setIsEditing(true)} className="text-gray-400 hover:text-indigo-400 transition-colors"><Pencil size={15} /></button>
          <button onClick={() => onDelete(task.id)} className="text-gray-400 hover:text-red-400 transition-colors"><Trash2 size={15} /></button>
        </>
      )}
    </div>
  );
}
jsx — src/App.jsx (hoàn chỉnh)
import { useState } from 'react';
import { useTasks } from './hooks/useTasks';
import { TaskForm } from './components/TaskForm';
import { TaskItem } from './components/TaskItem';

const FILTERS = ['Tất cả', 'Đang làm', 'Hoàn thành'];

export default function App() {
  const { tasks, addTask, toggleTask, deleteTask, editTask, clearCompleted } = useTasks();
  const [filter, setFilter] = useState('Tất cả');

  const filteredTasks = tasks.filter(t => {
    if (filter === 'Đang làm') return !t.completed;
    if (filter === 'Hoàn thành') return t.completed;
    return true;
  });

  const doneCount = tasks.filter(t => t.completed).length;

  return (
    <div className="min-h-screen bg-gray-900 text-white">
      <div className="max-w-xl mx-auto px-4 py-10">
        <h1 className="text-3xl font-bold mb-1">Task Manager</h1>
        <p className="text-gray-400 mb-6">{doneCount}/{tasks.length} công việc hoàn thành</p>

        <TaskForm onAdd={addTask} />

        {/* Filter tabs */}
        <div className="flex gap-2 mb-4">
          {FILTERS.map(f => (
            <button key={f} onClick={() => setFilter(f)}
              className={`px-3 py-1 rounded-full text-sm transition-colors ${filter === f ? 'bg-indigo-600 text-white' : 'text-gray-400 hover:text-white'}`}>
              {f}
            </button>
          ))}
        </div>

        {/* Task list */}
        {filteredTasks.length === 0 ? (
          <p className="text-center text-gray-500 py-10">Không có công việc nào.</p>
        ) : (
          filteredTasks.map(task => (
            <TaskItem key={task.id} task={task} onToggle={toggleTask} onDelete={deleteTask} onEdit={editTask} />
          ))
        )}

        {doneCount > 0 && (
          <button onClick={clearCompleted} className="mt-4 text-sm text-red-400 hover:text-red-300">
            Xóa {doneCount} công việc đã hoàn thành
          </button>
        )}
      </div>
    </div>
  );
}

Bước 5 — Build Và Deploy

bash — Build production và deploy lên Netlify/Vercel
# Build cho production
npm run build
# Output: thư mục dist/ chứa file tĩnh

# Preview bản build local
npm run preview

# Deploy lên Netlify (cách đơn giản nhất)
# 1. Cài Netlify CLI
npm install -g netlify-cli

# 2. Đăng nhập Netlify
netlify login

# 3. Deploy
netlify deploy --prod --dir=dist
# Nhận URL dạng: https://your-app.netlify.app

# Hoặc deploy lên Vercel
npm install -g vercel
vercel --prod
💡 Mẹo từ ThanhDoIT

6

Bài 6.6 — Design System & Component Library với AI

Thay vì code từng component riêng lẻ, senior developer build Design System — một bộ components tái sử dụng với style nhất quán. AI giúp bạn tạo Design System trong vài giờ thay vì vài tuần.

🎨
Design Tokens (CSS Variables)
  • Colors: primary, secondary, semantic
  • Typography: font sizes, weights, line heights
  • Spacing scale: 4px base unit
  • Border radius, shadows
🧱
Core Components
  • Button (variants: primary, secondary, ghost)
  • Input, Select, Checkbox, Radio
  • Card, Modal, Toast notifications
  • Loading states (Skeleton, Spinner)

Design Tokens — Nền Tảng Của Design System

css — design-tokens.css (root variables)
:root {
  /* === COLORS === */
  --color-primary-50:  #eff6ff;
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-700: #1d4ed8;

  --color-success: #10b981;
  --color-warning: #f59e0b;
  --color-error:   #ef4444;
  --color-info:    #6366f1;

  /* === NEUTRAL === */
  --color-gray-50:  #f9fafb;
  --color-gray-100: #f3f4f6;
  --color-gray-200: #e5e7eb;
  --color-gray-700: #374151;
  --color-gray-900: #111827;

  /* === TYPOGRAPHY === */
  --font-sans: 'Inter', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
  --text-xs:   0.75rem;   /* 12px */
  --text-sm:   0.875rem;  /* 14px */
  --text-base: 1rem;      /* 16px */
  --text-lg:   1.125rem;  /* 18px */
  --text-xl:   1.25rem;   /* 20px */
  --text-2xl:  1.5rem;    /* 24px */

  /* === SPACING (4px base) === */
  --space-1: 0.25rem;   /* 4px */
  --space-2: 0.5rem;    /* 8px */
  --space-3: 0.75rem;   /* 12px */
  --space-4: 1rem;      /* 16px */
  --space-6: 1.5rem;    /* 24px */
  --space-8: 2rem;      /* 32px */

  /* === RADIUS === */
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;
  --radius-full: 9999px;

  /* === SHADOWS === */
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
Dựa trên design tokens CSS này, hãy tạo cho tôi một Button component 
hoàn chỉnh với React + TypeScript:

Design tokens: --color-primary-500, --color-primary-600, --space-2, 
--space-4, --radius-md, --text-sm, --text-base

Requirements:
- Variants: primary | secondary | ghost | danger
- Sizes: sm | md | lg  
- States: hover, active, disabled, loading
- Props: variant, size, disabled, loading, leftIcon, rightIcon, onClick, children
- Loading state: hiện spinner, disable button, đổi text thành "Loading..."
- Accessible: aria-disabled, aria-busy khi loading
- Sử dụng design tokens, KHÔNG hardcode colors/spacing
Cung cấp design tokens cụ thể trong prompt → Copilot generate component nhất quán với hệ thống design của bạn.

React Component: Button Hoàn Chỉnh

typescript — Button.tsx
import React from 'react';
import './Button.css';

type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';

interface ButtonProps {
  variant?: ButtonVariant;
  size?: ButtonSize;
  loading?: boolean;
  disabled?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  children: React.ReactNode;
  onClick?: () => void;
  type?: 'button' | 'submit' | 'reset';
}

const Spinner = () => (
  <svg width="16" height="16" viewBox="0 0 24 24" className="btn-spinner">
    <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"
      fill="none" strokeDasharray="60" strokeDashoffset="15" />
  </svg>
);

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary', size = 'md', loading = false,
  disabled = false, leftIcon, rightIcon, children, onClick, type = 'button'
}) => {
  const isDisabled = disabled || loading;
  return (
    <button
      type={type}
      className={`btn btn--${variant} btn--${size}`}
      disabled={isDisabled}
      aria-disabled={isDisabled}
      aria-busy={loading}
      onClick={onClick}
    >
      {loading ? <Spinner /> : leftIcon && <span className="btn-icon">{leftIcon}</span>}
      <span>{loading ? 'Loading...' : children}</span>
      {!loading && rightIcon && <span className="btn-icon">{rightIcon}</span>}
    </button>
  );
};

export default Button;

Performance: useMemo & useCallback — Khi Nào Cần Dùng?

typescript — Performance hooks examples
import React, { useState, useMemo, useCallback } from 'react';

interface Product { id: number; name: string; price: number; category: string; }

const ProductList: React.FC<{ products: Product[] }> = ({ products }) => {
  const [search, setSearch] = useState('');
  const [category, setCategory] = useState('all');

  // useMemo: chỉ recalculate khi products, search, hoặc category thay đổi
  const filteredProducts = useMemo(() => {
    return products
      .filter(p => category === 'all' || p.category === category)
      .filter(p => p.name.toLowerCase().includes(search.toLowerCase()));
  }, [products, search, category]);

  // useMemo: tính toán expensive stats
  const stats = useMemo(() => ({
    total: filteredProducts.length,
    avgPrice: filteredProducts.reduce((sum, p) => sum + p.price, 0) / filteredProducts.length || 0,
    categories: [...new Set(products.map(p => p.category))]
  }), [filteredProducts, products]);

  // useCallback: tránh recreate handler function mỗi render
  const handleSearchChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value),
    [] // không có dependencies — function ổn định
  );

  return (
    <div>
      <input value={search} onChange={handleSearchChange} placeholder="Tìm sản phẩm..." />
      <p>{stats.total} sản phẩm | Giá TB: {stats.avgPrice.toFixed(0)}đ</p>
      {filteredProducts.map(p => <div key={p.id}>{p.name}</div>)}
    </div>
  );
};
⚠ Đừng Lạm Dụng useMemo/useCallback
  • Chỉ dùng khi: computation thực sự expensive (lọc mảng lớn, sort, format phức tạp) hoặc function được pass vào component đã memo hóa
  • Không cần: tính toán đơn giản, string/number operations — overhead của memo lớn hơn lợi ích
  • AI thường thêm useMemo/useCallback vào mọi chỗ — hỏi lại: "Optimization này có thực sự cần không với data size này?"
Tôi có React component sau đang bị lag khi render danh sách 500+ items.
Hãy phân tích và optimize:

[paste component code vào đây]

Yêu cầu:
1. Identify các re-render không cần thiết
2. Thêm React.memo cho child components phù hợp
3. Thêm useMemo cho expensive computations
4. Thêm useCallback cho event handlers được pass xuống
5. Nếu cần, suggest dùng virtual list (react-window) cho list quá dài
6. Giải thích tại sao mỗi optimization cần thiết
Paste code thực tế + yêu cầu giải thích → Copilot không chỉ fix mà còn dạy bạn tại sao.
🎯 Thực Hành: Build Mini Design System
  1. Tạo file design-tokens.css với color palette, spacing, typography của bạn
  2. Hỏi Copilot: "Generate Button component React với 4 variants dùng CSS variables này"
  3. Build thêm Input component với states: default, focus, error, disabled
  4. Build Card component: header, body, footer slots
  5. Tạo Modal component với backdrop, close button, keyboard ESC support
  6. Export tất cả từ src/components/index.ts — có thể import như: import { Button, Input, Card } from '@/components'
🎯 Bài Tập Tổng Kết Chương 6 — Frontend Portfolio
  1. Xây dựng Portfolio Website với React + Tailwind (Hero, Skills, Projects, Contact)
  2. Implement Dark/Light mode với CSS variables + localStorage preference
  3. Thêm animation: entrance animations với Intersection Observer
  4. Hỏi Copilot: "Optimize component này, chỉ dùng useMemo/useCallback khi thực sự cần"
  5. Deploy lên Vercel: npx vercel → có URL thật để show nhà tuyển dụng
  6. Thử thách: Thêm filter animation cho Projects section — lọc theo category với smooth transition
✅ Dấu Hiệu React Project Đạt Chất Lượng

🗒 Tóm Tắt Chương 6

← Trước đó
Chương 5: JavaScript/Node.js Tools
Zalo: 0898 619 966 Z Gọi: 0898 619 966