React 18 & TypeScript — Cặp Đôi Hoàn Hảo Cho Giao Diện 🎨¶
React thực chất là gì?¶
React là một thư viện JavaScript nổi tiếng giúp bạn xây dựng giao diện web (UI) bằng cách lắp ráp các mảnh ghép lại với nhau (gọi là component). Điểm hay nhất của nó là cách làm việc theo kiểu Khai báo (Declarative).
Thử so sánh nhé:
Cách cũ (Cầm tay chỉ việc - Imperative): Giống như bạn chỉ đường cho một người nhắm mắt. "Bước 1 bước, rẽ trái, kiếm cái nút này, tăng số lên 1, rồi in số đó ra...". Bạn phải chọc thẳng vào cấu trúc web (DOM) để sửa từng tí một.
// Phải chỉ rõ TỪNG BƯỚC thay đổi HTML
const btn = document.getElementById('count-btn');
let count = 0;
btn.addEventListener('click', () => {
count++;
document.getElementById('count-display').textContent = count;
});
Cách mới của React (Khai báo - Declarative): Giống như bạn đi ăn nhà hàng, bạn chỉ cần nói "Cho tôi đĩa cơm rang". Bạn chỉ cần mô tả giao diện trông như thế nào với dữ liệu hiện tại, React sẽ tự động làm phần việc tay chân là đi nấu nướng và cập nhật lên màn hình.
// Chỉ mô tả UI trông như thế nào với dữ liệu (state) hiện tại
function Counter() {
const [count, setCount] = useState(0);
// Giao diện = Một hàm của dữ liệu.
// React sẽ tự tính toán xem có gì thay đổi để cập nhật lên màn hình.
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
Thần chú nằm lòng: UI = f(state)
Giao diện chỉ đơn thuần là kết quả phản chiếu của dữ liệu. Khi dữ liệu (state) thay đổi → React sẽ chạy lại hàm → Lấy bản vẽ mới so với bản vẽ cũ (Virtual DOM) → Và chỉ cập nhật đúng cái vị trí bị thay đổi trên màn hình thật thôi. Rất thông minh!
Component (Những Mảnh Ghép Lego) 🧩¶
Trong React, mọi thứ đều là Component. Bạn cứ coi nó như một cái hàm bình thường: Nhận nguyên liệu đầu vào (gọi là props), và nhả ra giao diện HTML (gọi là JSX).
// Giao diện viết bằng Hàm (Đây là cách hiện đại, giờ người ta xài cái này hết)
interface Props {
name: string;
amount: number;
currency: string;
}
function TransactionRow({ name, amount, currency }: Props) {
// Khoản chi thì báo đỏ, khoản thu thì báo xanh
const isExpense = amount < 0;
return (
<div className={`flex justify-between ${isExpense ? 'text-red-600' : 'text-green-600'}`}>
<span>{name}</span>
<span>{formatCurrency(Math.abs(amount), currency)}</span>
</div>
);
}
// Lắp ráp mảnh ghép nhỏ vào mảnh ghép to hơn
function TransactionList({ transactions }) {
return (
<div>
{transactions.map(tx => (
<TransactionRow
key={tx.id} // ← key: Cái thẻ tên để React không bị nhầm lẫn giữa các dòng
name={tx.description}
amount={tx.type === 'expense' ? -tx.amount : tx.amount}
currency="VND"
/>
))}
</div>
);
}
Các Công Cụ Tiện Ích Cơ Bản (Hooks) 🧰¶
useState — Trí nhớ ngắn hạn của Component¶
function GroupPage() {
// Cú pháp: [tên_dữ_liệu, hàm_để_đổi_dữ_liệu] = useState(giá_trị_ban_đầu)
const [activeTab, setActiveTab] = useState<Tab>('transactions');
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setActiveTab('fund')}>Quỹ Nhóm</button>
<button onClick={() => setShowModal(true)}>Thêm giao dịch</button>
{/* Dữ liệu đổi thì giao diện đổi theo */}
{activeTab === 'transactions' && <TransactionList />}
{activeTab === 'fund' && <FundTab />}
{showModal && (
<AddTransactionModal onClose={() => setShowModal(false)} />
)}
</div>
);
}
Tuyệt đối không được sửa lén dữ liệu!
useEffect — Làm việc riêng sau cánh gà (Side Effects)¶
function GroupPage() {
const { groupId } = useParams();
useEffect(() => {
// Việc này sẽ chạy ngay sau khi giao diện đã được vẽ xong, hoặc khi ID nhóm bị đổi
document.title = `Nhóm ${groupId}`;
// Hàm dọn dẹp (Chạy trước khi bị hủy hoặc trước khi ID nhóm đổi sang số khác)
return () => {
document.title = 'Phần Mềm Quản Lý Quỹ';
};
}, [groupId]); // <-- Danh sách theo dõi: Chỉ chạy lại nếu cái 'groupId' này thay đổi
}
Mẹo nhỏ cho dự án này
Thường người ta hay xài useEffect để gọi API. Nhưng dự án của chúng ta đã có siêu quản gia TanStack Query lo vụ đó rồi. Nên bạn sẽ thấy useEffect chỉ dùng cho vài việc lặt vặt thôi, đỡ nhức đầu!
useRef — Ghi nhớ mà không làm phiền¶
function TurnstileWidget({ onVerify }) {
// Tạo một điểm neo để xíu nữa móc vào HTML
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
// Treo công cụ Turnstile thẳng vào cái thẻ Div đó
window.turnstile.render(containerRef.current, { ... });
}, []);
return <div ref={containerRef} />;
}
Note: Khác với useState, khi useRef thay đổi giá trị, nó sẽ im ru và không ép React phải vẽ lại giao diện.
JSX — Con lai giữa JavaScript và HTML 🧬¶
Trông nó y hệt thẻ HTML, nhưng thực ra trình duyệt không hiểu JSX đâu. Nó sẽ được dịch ngược về JavaScript nguyên thủy:
// Bạn viết thế này (JSX):
const element = <h1 className="title">Chào {name}</h1>;
// Máy nó tự dịch ra thành thế này:
const element = React.createElement('h1', { className: 'title' }, 'Chào ', name);
Những cú lừa nhẹ so với HTML thông thường:
| Thẻ HTML thường | Khi viết bằng JSX |
|---|---|
class="..." |
className="..." (Do chữ class bị trùng với từ khóa của JS) |
for="..." |
htmlFor="..." |
onclick="..." |
onClick={...} (Viết hoa chữ cái đầu của từ tiếp theo - camelCase) |
style="color: red" |
style={{ color: 'red' }} (Phải đưa vào một object) |
| `` | {/* Ghi chú trong này */} |
React 18 — Có gì hot? (Concurrent Mode) 🚀¶
Phiên bản 18 mang đến tính năng "Đa nhiệm" (Concurrent Rendering). React giờ đây có thể rảnh tay tạm dừng một việc hiển thị đang dang dở để ưu tiên làm việc khác quan trọng hơn.
// web/src/main.tsx
import { createRoot } from 'react-dom/client'; // Xài công cụ đời mới của React 18
// Dùng createRoot thay cho ReactDOM.render ngày xưa
createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Về cái thẻ StrictMode: Lúc bạn đang code ở máy nhà (Development), React sẽ dở chứng render mọi component 2 lần liên tiếp. Nó cố tình làm thế để soi xem bạn có viết code gây lỗi ngầm không. Yên tâm là lúc đẩy lên mạng thật (Production) nó chỉ chạy 1 lần thôi nhé.
Mặc Áo Giáp TypeScript Cho React 🛡️¶
TypeScript giúp bạn khai báo rõ ràng "Tôi muốn cái gì", nhập sai phát là nó gạch chân đỏ báo lỗi ngay lúc gõ code. Đỡ phải chờ lúc chạy mới nổ lỗi.
Gắn mác cho Dữ liệu truyền vào (Props)¶
// Định nghĩa trước là cái thẻ này cần nhận những thông tin gì
interface GroupCardProps {
id: string;
name: string;
currency: string;
memberRole: 'admin' | 'member'; // Chỉ được phép là 1 trong 2 chữ này
onSelect?: (id: string) => void; // Thêm dấu hỏi (?) nghĩa là có cũng được không có cũng chả sao
}
function GroupCard({ id, name, currency, memberRole, onSelect }: GroupCardProps) {
return (
// Dấu ?. giúp báo: Nếu người ta truyền hàm onSelect thì mới gọi, không thì thôi
<div onClick={() => onSelect?.(id)}>
<h3>{name}</h3>
<span>{currency}</span>
{memberRole === 'admin' && <span>Trùm Nhóm</span>}
</div>
);
}
Gắn mác cho Sự kiện (Events)¶
function LoginForm() {
// Ép kiểu cho hành động bấm nút Submit của Form
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // Chặn hành vi tải lại trang mặc định của HTML
const form = e.currentTarget;
const email = (form.elements.namedItem('email') as HTMLInputElement).value;
};
// Ép kiểu cho hành động gõ phím vào ô Text
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
};
return (
<form onSubmit={handleSubmit}>
<input name="email" onChange={handleChange} />
</form>
);
}
Component Đa Năng (Generic)¶
Cái này hơi nâng cao. Giống như bạn tạo một cái khuôn chung chung, lúc gọi hàm bạn bơm kiểu chữ vào thì nó ra chữ, bơm kiểu số vào thì nó nhận số.
// Một cái menu xổ xuống đa năng
interface SelectProps<T> {
options: T[];
getValue: (item: T) => string;
getLabel: (item: T) => string;
onChange: (value: string) => void;
}
// Chữ <T> đại diện cho một kiểu dữ liệu bí ẩn nào đó sẽ được quyết định sau
function Select<T>({ options, getValue, getLabel, onChange }: SelectProps<T>) {
return (
<select onChange={e => onChange(e.target.value)}>
{options.map(item => (
<option key={getValue(item)} value={getValue(item)}>
{getLabel(item)}
</option>
))}
</select>
);
}
Sắp Xếp Đồ Đạc Trong Dự Án (Cấu trúc thư mục) 📁¶
web/src/
pages/ ← Chứa những Component to đùng dùng làm từng Trang web riêng biệt
GroupPage.tsx
GroupsListPage.tsx
LoginPage.tsx
...
components/ ← Chứa những mảnh ghép nhỏ xài đi xài lại được nhiều nơi
AddTransactionModal.tsx
TransactionList.tsx
TopNav.tsx
...
lib/ ← Chứa những hàm tính toán linh tinh (Không phải là giao diện)
api.ts
format.ts
store/ ← Chứa "trí nhớ dài hạn" của web bằng Zustand
authStore.ts
Luật giang hồ bất thành văn (Naming conventions):
- File nào chứa Giao diện (Component): Viết hoa chữ cái đầu tiên
PascalCase.tsx. - File nào chứa mấy Hàm tính toán tiện ích: Viết thường kiểu con lạc đà
camelCase.ts. - Tốt nhất là mỗi file chỉ nên xuất ra (
export default) một cái Component chính thôi cho nó gọn gàng dễ tìm.