# Bài 09 — React căn bản
⬅️ Bài trước | Mục lục | Bài tiếp theo ➡️
🎯 Mục tiêu
Nắm đủ React để đọc và viết được mọi component trong dự án:
- Component & JSX
- Props — truyền dữ liệu xuống
- State &
useState— dữ liệu thay đổi được useEffect,useMemo,useRef— các hook dự án dùng- Render danh sách, render có điều kiện, xử lý sự kiện, form có kiểm soát
Mọi ví dụ đều lấy từ code thật của dự án.
📚 1. Component là gì?
Component là một hàm JavaScript trả về giao diện.
function ChaoHoi() {
return <h1>Xin chào!</h1>;
}
Dùng nó như một thẻ HTML:
<ChaoHoi />
Ba quy tắc bắt buộc
-
Tên component phải viết hoa chữ đầu
function product() {} // ❌ React hiểu là thẻ HTML <product> function Product() {} // ✅ React hiểu là component⚠️ Source gốc có
export default function navbar()— chữ thường! Nó vẫn chạy vì được import với tênNabar(viết hoa) tronglayout.js. Nhưng đây là thói quen xấu. Nên đặtNavbar. -
Phải
returnmột phần tử gốc duy nhất// ❌ Sai — 2 phần tử ngang hàng return ( <h1>A</h1> <p>B</p> ); // ✅ Bọc trong Fragment <> </> return ( <> <h1>A</h1> <p>B</p> </> );<>...</>là Fragment — nhóm phần tử lại mà không tạo thẻ HTML thừa. Dự án dùng rất nhiều. -
Component phải "thuần" — cùng input thì cùng output, không sửa dữ liệu bên ngoài.
📚 2. JSX — HTML trong JavaScript
JSX trông giống HTML nhưng thực chất là JavaScript.
Nhúng biểu thức bằng {}
const ten = "Gucci";
const gia = 5050000;
return (
<div>
<h5>{ten}</h5>
<p>{gia.toLocaleString()}đ</p>
<p>{gia > 4000000 ? "Cao cấp" : "Phổ thông"}</p>
</div>
);
Trong {} bạn viết được biểu thức (có giá trị), không viết được câu lệnh
(if, for, return).
Khác biệt so với HTML
| HTML | JSX | Lý do |
|---|---|---|
class="btn" |
className="btn" |
class là từ khóa của JS |
for="name" |
htmlFor="name" |
for là từ khóa của JS |
onclick="..." |
onClick={...} |
JSX dùng camelCase |
<br> |
<br /> |
Mọi thẻ phải đóng |
style="width: 60px" |
style={{ width: "60px" }} |
style là object |
tabindex |
tabIndex |
camelCase |
Thuộc tính style
<img style={{ width: 60 + "px" }} src="/img/h1.png" />
// ↑↑ 2 dấu ngoặc: ngoài = nhúng JS, trong = object
Cách viết trong dự án { width: 60 + "px" } hơi lạ. Ba cách sau tương đương:
style={{ width: "60px" }} // rõ ràng nhất
style={{ width: 60 }} // React tự thêm "px" cho các thuộc tính số
style={{ width: 60 + "px" }} // cách dự án dùng
⚠️ Lỗi phổ biến trong source gốc: dùng class thay vì className
Rất nhiều chỗ trong dự án viết class thay vì className:
// frontend/app/(user)/component/navbar.js
<a class="navbar-brand offset-md-1" href="#"> {/* ❌ class */}
<i class="fa-solid fa-mug-hot"></i> {/* ❌ class */}
// frontend/app/(user)/menu/layout.js
<div class="card"> {/* ❌ class */}
<ul class="list-group list-group-flush"> {/* ❌ class */}
React sẽ cảnh báo trong console:
Warning: Invalid DOM property `class`. Did you mean `className`?
Trong React 19 trở lên, class được chuyển thành className tự động, nên vẫn hiển thị đúng.
Nhưng console đầy warning và đây không phải cách viết chuẩn.
Cách sửa hàng loạt: dùng Find & Replace trong VS Code với regex:
- Find:
\sclass=" - Replace:
className="
Tương tự với for= → htmlFor=, tabindex → tabIndex, crossorigin → crossOrigin.
📚 3. Props — truyền dữ liệu từ cha xuống con
// Component con
function Product(props) {
const product = props.data;
return <h5>{product.name}</h5>;
}
// Component cha
<Product data={item} />
Trong dự án
// frontend/app/(user)/component/product.js
export default function Product(props) {
const product = props.data;
return (
<div className="card mb-2" style={{ width: 18 + "rem" }}>
<img src={`${process.env.NEXT_PUBLIC_IMA_URL}${product.image}`} className="card-img-top" />
<div className="card-body">
<h5 className="card-title">{product.name}</h5>
<p className="card-text">{product.price}đ</p>
</div>
</div>
);
}
Destructuring props (cách gọn hơn)
// frontend/app/(user)/component/addcardbuton.js
export default function AddCardbutton(props) {
const { product, quantity, size, className } = props;
// ...
}
// Hoặc bóc ngay tại tham số:
export default function AddCardbutton({ product, quantity, size, className }) {
// ...
}
Props children
Mọi thứ nằm giữa thẻ mở và thẻ đóng sẽ vào props.children:
<AddCardbutton product={p} quantity={1} size="s">
Đặt Mua {/* ← đây là children */}
</AddCardbutton>
⚠️ Bug trong dự án:
AddCardbuttonnhậnchildrennhưng không dùng:export default function AddCardbutton(props) { const { product, quantity, size, className } = props; // không lấy children return <button ...>đặt mua</button>; // hardcode chữ }Nên dù truyền "Đặt Mua" hay "thêm vào giỏ hàng", nút luôn hiện chữ "đặt mua" (viết thường).
Cách sửa:
export default function AddCardbutton({ product, quantity, size, className, children }) { const dispatch = useDispatch(); return ( <button onClick={() => dispatch(addItem({ product, quantity, size }))} className={`btn btn-dark w-100 ${className}`} > {children} </button> ); }
Props là chỉ đọc
function Product(props) {
props.data.name = "Đổi tên"; // ❌ TUYỆT ĐỐI KHÔNG
}
Dữ liệu chảy một chiều từ cha xuống con. Muốn con báo lên cha, cha truyền xuống một hàm callback.
📚 4. State và useState
Props là dữ liệu cha đưa xuống (không đổi được). State là dữ liệu của chính component (đổi được, và khi đổi thì component render lại).
import { useState } from "react";
function DemQuantity() {
const [quantity, setQuantity] = useState(1);
// ↑giá trị ↑hàm cập nhật ↑giá trị ban đầu
return (
<div>
<p>Số lượng: {quantity}</p>
<button onClick={() => setQuantity(quantity + 1)}>+</button>
<button onClick={() => setQuantity(quantity - 1)}>−</button>
</div>
);
}
Trong dự án
// frontend/app/(user)/product/[id]/page.js
const [quantity, setquantity] = useState(1);
const [size, setsize] = useState("s");
<select onChange={(e) => setsize(e.target.value)} className="form-select">
<option value="s">S</option>
<option value="m">M</option>
<option value="l">L</option>
</select>
<input onChange={(e) => setquantity(e.target.value)} type="number" defaultValue={1} />
// frontend/app/(user)/cart/page.js
const [fullname, setfullname] = useState("");
const [phone, setphone] = useState("");
const [address, setaddress] = useState("");
Ba quy tắc quan trọng của state
1. KHÔNG BAO GIỜ gán trực tiếp
quantity = 5; // ❌ React không biết → không render lại
setQuantity(5); // ✅
2. setState là bất đồng bộ
setQuantity(quantity + 1);
console.log(quantity); // ← vẫn là giá trị CŨ!
React gom nhiều lần cập nhật lại rồi render một lần. Muốn dùng giá trị mới, đợi lần render kế tiếp.
3. Cập nhật dựa trên giá trị cũ thì dùng hàm
setQuantity(quantity + 1);
setQuantity(quantity + 1);
// → chỉ tăng 1! Vì cả 2 đều dùng `quantity` cũ
setQuantity((prev) => prev + 1);
setQuantity((prev) => prev + 1);
// → tăng 2 ✅
4. Với object/mảng, phải tạo bản mới
const [cart, setCart] = useState([]);
cart.push(item); // ❌ Sửa mảng cũ, React không thấy thay đổi
setCart([...cart, item]); // ✅ Mảng mới
user.name = "A"; // ❌
setUser({ ...user, name: "A" }); // ✅
React so sánh bằng tham chiếu (===). Cùng một mảng thì === cho true
→ React nghĩ không có gì đổi → không render lại.
💡 Redux Toolkit (bài 13) có thư viện Immer bên trong, cho phép bạn "viết như đang sửa trực tiếp" mà vẫn tạo ra bản mới. Đó là ngoại lệ duy nhất bạn sẽ gặp.
📚 5. Xử lý sự kiện
<button onClick={() => console.log("click")}>Bấm</button>
⚠️ Bẫy: gọi hàm thay vì truyền hàm
<button onClick={handleClick}>OK</button> // ✅ truyền hàm
<button onClick={handleClick()}>OK</button> // ❌ GỌI ngay khi render!
<button onClick={() => handleClick(id)}>OK</button> // ✅ có tham số thì bọc arrow
Đối tượng sự kiện e
<input onChange={(e) => setQuantity(e.target.value)} />
// ↑ e.target là phần tử DOM, .value là giá trị hiện tại
// frontend/app/(user)/cart/page.js
const submit = (e) => {
e.preventDefault(); // ← ngăn form reload lại trang
fetch(...);
};
<form onSubmit={submit}>...</form>
e.preventDefault() bắt buộc với form. Không có nó, trình duyệt reload trang
và mọi state biến mất.
📚 6. Render danh sách
{data.map((item) => (
<div key={item._id} className="col">
<Product data={item} />
</div>
))}
key — không được quên
React dùng key để biết phần tử nào được thêm/xóa/di chuyển.
{data.map((item) => <Product key={item._id} data={item} />)} // ✅ id duy nhất
{data.map((item, i) => <Product key={i} data={item} />)} // ⚠️ tạm được
{data.map((item) => <Product data={item} />)} // ❌ thiếu key
Dùng index làm key gây bug khi danh sách được sắp xếp lại hoặc xóa —
đúng tình huống của trang menu trong dự án. Luôn ưu tiên _id.
⚠️ Bug tiềm ẩn trong giỏ hàng:
// frontend/app/(user)/cart/page.js {cart.map(product => ( <tr key={product._id} className="align-middle">Cùng một sản phẩm với 2 size khác nhau là 2 dòng riêng nhưng có cùng
_id→ key trùng → React cảnh báo và có thể render sai.Cách sửa:
key={{product.size}}
📚 7. Render có điều kiện
Toán tử &&
{product.rating > 0 && (
<small className="text-warning fw-bold me-1">
{product.rating}<i className="fa-solid fa-star"></i>
</small>
)}
Toán tử 3 ngôi
{isLoading ? <p>Loading...</p> : <Productlist data={data} />}
Return sớm (early return) — cách dự án dùng nhiều nhất
// frontend/app/(user)/menu/page.js
export default function Menu() {
const { data: productlist, error, isLoading } = useSWR(url, fetcher);
if (error) return <strong>đã có lỗi</strong>;
if (isLoading) return <p>Loading...</p>;
return <Productlist data={productlist} />;
}
Cách này rất sạch: xử lý các trường hợp đặc biệt trước, phần chính ở cuối.
⚠️ Quy tắc Hooks: mọi
useState/useEffect/useSWRphải được gọi TRƯỚC mọi câureturn. Nếu không, số lượng hook thay đổi giữa các lần render và React sẽ báo lỗi:React has detected a change in the order of Hooks called by MenuDự án làm đúng điều này ở mọi component.
📚 8. Form có kiểm soát (controlled input)
const [fullname, setfullname] = useState("");
<input
type="text"
value={fullname} // giá trị đến từ state
onChange={(e) => setfullname(e.target.value)} // mọi thay đổi cập nhật state
/>
Đây gọi là "controlled": React là nguồn sự thật duy nhất về giá trị của input.
value vs defaultValue
| Ai kiểm soát | Khi nào dùng | |
|---|---|---|
value + onChange |
React (controlled) | Cần đọc/validate/reset giá trị |
defaultValue |
DOM (uncontrolled) | Chỉ cần giá trị khởi tạo |
Dự án dùng uncontrolled ở form đặt hàng:
// frontend/app/(user)/cart/page.js — không có value=
<input type="text" className="form-control" id="fullname"
onChange={(e) => setfullname(e.target.value)} />
Hệ quả: sau khi đặt hàng thành công, dispatch(removeCart()) xóa giỏ nhưng
các ô input vẫn giữ chữ cũ, vì React không kiểm soát chúng.
Cách sửa: thêm value={fullname} rồi gọi setfullname("") sau khi submit.
⚠️ Đừng để
valuemà thiếuonChange— input sẽ bị "khóa cứng", gõ không được, và React cảnh báo:You provided a `value` prop to a form field without an `onChange` handler
📚 9. Các hook khác dự án dùng
useEffect — chạy code sau khi render
import { useEffect } from "react";
useEffect(() => {
console.log("Chạy sau mỗi lần render");
});
useEffect(() => {
console.log("Chỉ chạy 1 lần khi component mount");
}, []); // ← mảng phụ thuộc rỗng
useEffect(() => {
console.log("Chạy khi productId thay đổi");
}, [productId]);
useEffect(() => {
const timer = setInterval(() => {}, 1000);
return () => clearInterval(timer); // ← hàm dọn dẹp, chạy khi unmount
}, []);
Dự án không dùng useEffect để gọi API — nó dùng SWR (bài 12), gọn hơn nhiều.
Nhưng bạn sẽ cần useEffect ở bài 19 (kiểm tra token) và bài 21 (debounce tìm kiếm).
useMemo — ghi nhớ kết quả tính toán
// frontend/app/(user)/component/navbar.js
const cart = useSelector((state) => state.cart);
const totalitem = useMemo(() => {
return cart.reduce((total, item) => total + item.quantity, 0);
}, [cart]); // ← chỉ tính lại khi `cart` đổi
Không có useMemo, phép reduce chạy lại mỗi lần navbar render —
kể cả khi chỉ có state khác thay đổi.
💡 Đừng lạm dụng
useMemo. Chỉ dùng khi phép tính thật sự nặng hoặc kết quả được truyền làm props cho component đã tối ưu. Với mảng 5 phần tử,useMemocòn tốn hơn là tính trực tiếp.
useRef — giữ giá trị không gây render lại
Hai công dụng:
a) Tham chiếu tới phần tử DOM
// frontend/app/(user)/cart/page.js
const closeBtn = useRef();
<button ref={closeBtn} data-bs-dismiss="modal"></button>
// Sau khi đặt hàng xong, "bấm" nút đóng modal bằng code:
closeBtn.current.click();
b) Lưu giá trị qua các lần render mà không kích hoạt render
// frontend/app/(user)/component/productlist.js
let defaultData = useRef([...props.data]);
⚠️ Đây chính là bug lớn nhất của dự án.
useRefchỉ lấy giá trị khởi tạo ở lần render đầu tiên. Khi người dùng chuyển sang danh mục khác,props.datađổi nhưngdefaultData.currentvẫn là dữ liệu cũ. → Bấm nút "Mặc Định" hiển thị sản phẩm của danh mục trước đó!Bài 15 sẽ mổ xẻ và sửa triệt để.
So sánh useState và useRef
useState |
useRef |
|
|---|---|---|
| Đổi giá trị → render lại? | ✅ Có | ❌ Không |
| Cách đọc | value |
ref.current |
| Cách ghi | setValue(x) |
ref.current = x |
| Dùng khi | Dữ liệu hiển thị ra UI | Tham chiếu DOM, timer id, giá trị nội bộ |
📚 10. Vòng đời một component
1. MOUNT (lần đầu xuất hiện)
├─ Chạy thân hàm component
├─ useState khởi tạo giá trị ban đầu
├─ useRef khởi tạo .current
├─ Trả về JSX → React vẽ ra DOM
└─ useEffect(..., []) chạy
2. UPDATE (state hoặc props đổi)
├─ Chạy lại thân hàm component
├─ useState trả về giá trị MỚI NHẤT
├─ useRef giữ nguyên .current (KHÔNG khởi tạo lại) ← nguồn gốc bug ở mục 9
├─ React so sánh JSX cũ/mới, chỉ cập nhật phần khác
└─ useEffect chạy nếu dependency đổi
3. UNMOUNT (biến mất khỏi màn hình)
└─ Hàm dọn dẹp trong useEffect chạy
📝 Bài tập
-
Component
Counter: hiển thị số, có nút+,−,Reset. Không cho số nhỏ hơn 0. -
Component
ProductCard: nhận props{ name, price, image }, hiển thị dạng card Bootstrap. Nếuprice > 4000000thì hiện thêm badge "Cao cấp". -
Sửa bug
AddCardbutton: làm cho nút hiển thị đúngchildrenđược truyền vào. -
Sửa
class→className: dùng Find & Replace với regex trên toàn bộ thư mụcfrontend/app/. Sau đó mở Console trình duyệt, đếm xem còn bao nhiêu warning. -
Form đăng ký có kiểm soát: sửa
app/(user)/register/page.jsthành controlled form với 3 state (name,email,password), thêm nút "Xóa hết" để reset về rỗng. Hiển thị lỗi nếu email không chứa@. -
Tự tạo bug
useRef:function Demo({ value }) { const [count, setCount] = useState(0); const refValue = useRef(value); return ( <div> <p>Props: {value} | Ref: {refValue.current}</p> <button onClick={() => setCount(count + 1)}>Render lại</button> </div> ); }Truyền
valuethay đổi từ component cha, rồi bấm "Render lại". Quan sátRefcó thay đổi không. Đây chính là bug ởproductlist.js.
⬅️ Bài trước | Mục lục | Bài tiếp theo: Next.js App Router ➡️
All rights reserved