0

# 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

  1. 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ên Nabar (viết hoa) trong layout.js. Nhưng đây là thói quen xấu. Nên đặt Navbar.

  2. Phải return mộ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>
      </>
    );
    

    <>...</>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.

  3. 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=, tabindextabIndex, crossorigincrossOrigin.


📚 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: AddCardbutton nhận children như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.id{product._id}-{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/useSWR phải được gọi TRƯỚC mọi câu return. 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 Menu

Dự á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 để value mà thiếu onChange — 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ử, useMemo cò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. useRef chỉ 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ưng defaultData.current vẫ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 useStateuseRef

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

  1. Component Counter: hiển thị số, có nút +, , Reset. Không cho số nhỏ hơn 0.

  2. Component ProductCard: nhận props { name, price, image }, hiển thị dạng card Bootstrap. Nếu price > 4000000 thì hiện thêm badge "Cao cấp".

  3. Sửa bug AddCardbutton: làm cho nút hiển thị đúng children được truyền vào.

  4. Sửa classclassName: dùng Find & Replace với regex trên toàn bộ thư mục frontend/app/. Sau đó mở Console trình duyệt, đếm xem còn bao nhiêu warning.

  5. Form đăng ký có kiểm soát: sửa app/(user)/register/page.js thà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 @.

  6. 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 value thay đổi từ component cha, rồi bấm "Render lại". Quan sát Ref có 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí