0

# Bài 02 — JavaScript cần thiết cho dự án

⬅️ Bài trước | Mục lục | Bài tiếp theo ➡️


🎯 Mục tiêu

Bài này không dạy JavaScript từ A–Z. Nó dạy đúng những cú pháp xuất hiện trong dự án — để khi đọc code ở bài 04 trở đi, bạn không bị "khựng" ở dòng nào.

Mọi ví dụ dưới đây đều trích từ code thật của dự án.

💡 Cách học bài này: mở terminal, gõ node để vào chế độ tương tác, rồi gõ thử từng đoạn code. Học bằng tay nhớ lâu hơn học bằng mắt.


1. var, let, const — khai báo biến

var  x = 1;   // Kiểu cũ, tránh dùng
let  y = 2;   // Có thể gán lại
const z = 3;  // Không gán lại được

Quy tắc thực hành: mặc định dùng const. Chỉ đổi sang let khi thật sự cần gán lại.

Trong dự án bạn sẽ thấy cả var (ở file do express-generator sinh ra) lẫn const:

// backend/routes/products.js — dòng đầu, do generator sinh, dùng var
var express = require("express");
var router = express.Router();

// Code do người viết thêm, dùng const
const connectDb = require("../model/db");
const { ObjectId } = require("mongodb");

⚠️ const với object/mảng chỉ khóa biến, không khóa nội dung:

const cart = [];
cart.push("A");   // ✅ Được — thay đổi nội dung
cart = ["B"];     // ❌ Lỗi — gán lại biến

2. Arrow function — hàm mũi tên

Ba cách viết dưới đây tương đương nhau:

// Cách cũ
function cong(a, b) {
  return a + b;
}

// Arrow function đầy đủ
const cong = (a, b) => {
  return a + b;
};

// Arrow function rút gọn — 1 biểu thức thì tự return
const cong = (a, b) => a + b;

Xuất hiện trong dự án ở đâu?

// frontend/app/(user)/page.js — hàm fetcher của SWR
const fetcher = (...agrs) => fetch(...agrs).then((res) => res.json());

Đọc từng phần:

  • (...agrs) — nhận bao nhiêu tham số cũng được, gom thành mảng agrs (xem mục 5)
  • => fetch(...) — không có {} nên tự động return kết quả của fetch(...)
  • .then((res) => res.json()) — khi fetch xong, lấy res rồi trả về res.json()

Trả về object phải bọc trong ngoặc tròn

const taoUser = (ten) => ({ name: ten });   // ✅ Đúng
const taoUser = (ten) => { name: ten };     // ❌ Sai — JS hiểu {} là thân hàm

3. Template literal — chuỗi có nhúng biến

Dùng dấu backtick ` (phím bên trái số 1) thay vì nháy đơn/kép:

const ten = "Gucci";
const gia = 5050000;

// Cách cũ — nối chuỗi
const s1 = "Sản phẩm " + ten + " giá " + gia + "đ";

// Template literal — dễ đọc hơn nhiều
const s2 = `Sản phẩm ${ten} giá ${gia}đ`;

Trong dự án

// URL API được ghép từ biến môi trường
useSWR(`${process.env.NEXT_PUBLIC_API_URL}/products`, fetcher)

// URL ảnh
<img src={`${process.env.NEXT_PUBLIC_IMA_URL}${product.image}`} />

// Class CSS có điều kiện
<Link className={`nav-link ${patname == "/" ? "active" : " "}`} href="/">

Cái cuối kết hợp cả toán tử 3 ngôi (mục 8) — rất hay gặp trong React.


4. Destructuring — bóc tách object và mảng

Đây là cú pháp bạn sẽ gặp nhiều nhất trong dự án.

Với object

const product = { name: "Gucci Flora", price: 5050000, rating: 4.5 };

// Cách cũ
const name = product.name;
const price = product.price;

// Destructuring — ngắn hơn nhiều
const { name, price } = product;

Đổi tên khi bóc tách

const { name: tenSanPham, price: giaBan } = product;
console.log(tenSanPham);  // "Gucci Flora"

Trong dự án, cú pháp này dùng để đặt tên rõ ràng khi gọi SWR nhiều lần trong một component:

// frontend/app/(user)/menu/layout.js
const {
  data: category,               // đổi `data` thành `category`
  error: errorcategory,         // đổi `error` thành `errorcategory`
  isLoading: isLoadingcategory
} = useSWR(`${process.env.NEXT_PUBLIC_API_URL}/categories`, fetcher);

Nếu không đổi tên, gọi SWR lần thứ hai trong cùng component sẽ bị trùng biến data.

Bóc tách ngay ở tham số hàm

// frontend/app/(user)/component/addcardbuton.js
export default function AddCardbutton(props) {
  const { product, quantity, size, className } = props;
  // ...
}

// Hoặc gọn hơn — bóc ngay tại tham số:
export default function AddCardbutton({ product, quantity, size, className }) {
  // ...
}
// frontend/app/(user)/product/[id]/page.js
export default function Productdetail({ params }) {
  // params.id chính là phần [id] trên URL
}

Bóc tách khi import

const { ObjectId } = require("mongodb");
// tương đương: const ObjectId = require("mongodb").ObjectId;

Với mảng

const [min, max] = [0, 100];
console.log(min);  // 0

useState của React trả về một mảng 2 phần tử, nên ta bóc tách như sau:

const [quantity, setquantity] = useState(1);
//     ↑giá trị   ↑hàm cập nhật

5. Spread & Rest — dấu ba chấm ...

Cùng ký hiệu ... nhưng có 2 công dụng ngược nhau.

Spread — "trải" ra

const a = [1, 2, 3];
const b = [...a, 4];        // [1, 2, 3, 4] — sao chép mảng a rồi thêm 4

const p = { name: "Gucci", price: 100 };
const q = { ...p, quantity: 2 };   // { name: "Gucci", price: 100, quantity: 2 }

Trong dự án:

// redux/slices/cartSlicer.js — thêm quantity & size vào bản sao sản phẩm
cart.push({ ...product, quantity, size });

// component/productlist.js — sao chép mảng trước khi lưu
let defaultData = useRef([...props.data]);

💡 [...a] tạo ra mảng mới, khác với a. Đây gọi là "shallow copy" — rất quan trọng trong React và Redux, vì React chỉ render lại khi thấy tham chiếu thay đổi.

Rest — "gom" lại

function tong(...cacSo) {
  return cacSo.reduce((t, n) => t + n, 0);
}
tong(1, 2, 3);   // 6 — cacSo = [1, 2, 3]

Trong dự án:

const fetcher = (...agrs) => fetch(...agrs).then((res) => res.json());
//                ↑rest: gom mọi tham số   ↑spread: trải ra để truyền cho fetch

Viết vậy để fetcher truyền lại y nguyên mọi tham số mà SWR đưa vào cho fetch.


6. Các phương thức mảng quan trọng

Đây là "vũ khí chính" của bạn khi làm việc với danh sách sản phẩm.

Dữ liệu mẫu dùng chung cho các ví dụ:

const products = [
  { name: "Gucci Flora",  price: 5050000, rating: 4.5 },
  { name: "Scandal",      price: 4850000, rating: 4.5 },
  { name: "Versace Eros", price: 3480000, rating: 4.0 },
];

.map() — biến đổi từng phần tử, trả mảng mới cùng độ dài

const ten = products.map((p) => p.name);
// ["Gucci Flora", "Scandal", "Versace Eros"]

Trong React, .map() dùng để biến mảng dữ liệu thành mảng JSX:

// frontend/app/(user)/component/productlist.js
{data.map((item) => (
  <div key={item._id} className="col">
    <Product data={item} />
  </div>
))}

⚠️ Luôn có key khi map ra JSX. React dùng key để biết phần tử nào thay đổi. Thiếu key → cảnh báo trong console và có thể render sai.

.filter() — giữ lại phần tử thỏa điều kiện, trả mảng ngắn hơn hoặc bằng

const daT = products.filter((p) => p.price > 4000000);
// [Gucci Flora, Scandal]

Trong dự án — lọc theo khoảng giá:

// frontend/app/(user)/component/productlist.js
data = data.filter((item) => min <= item.price && item.price <= max);

.find() — tìm phần tử đầu tiên thỏa điều kiện, trả về 1 phần tử (hoặc undefined)

const sp = products.find((p) => p.name === "Scandal");
// { name: "Scandal", price: 4850000, rating: 4.5 }

Trong dự án — ghép danh mục vào sản phẩm:

// backend/routes/products.js
const category = categories.find(
  (cat) => cat._id.toString() === item.categoryId.toString()
);
item.category = category;

.findIndex() — trả về vị trí (số), hoặc -1 nếu không có

// redux/slices/cartSlicer.js
const index = cart.findIndex(
  (item) => item._id == product._id && item.size == size
);
if (index == -1) {
  cart.push({ ...product, quantity, size });   // chưa có → thêm mới
} else {
  cart[index].quantity += Number(quantity);    // đã có → cộng dồn
}

Đây chính là logic "thêm vào giỏ hàng" của dự án. Đọc kỹ đoạn này!

.reduce() — gom cả mảng thành một giá trị

Cú pháp: arr.reduce((tichLuy, phanTuHienTai) => giaTriMoi, giaTriBanDau)

const tongTien = products.reduce((tong, p) => tong + p.price, 0);
// 0 + 5050000 → 5050000 + 4850000 → 9900000 + 3480000 → 13380000

Trong dự án — 2 chỗ dùng reduce:

// component/navbar.js — đếm tổng số món trong giỏ
const totalitem = cart.reduce((total, item) => total + item.quantity, 0);

// cart/page.js — tính tổng tiền
const total = cart.reduce((total, item) => total + item.price * item.quantity, 0);

.sort() — sắp xếp

const tangDan = [...products].sort((a, b) => a.price - b.price);
const giamDan = [...products].sort((a, b) => b.price - a.price);

Quy tắc hàm so sánh: trả về số âm thì a đứng trước, số dương thì b đứng trước.

⚠️ .sort() thay đổi mảng gốc! Đây là bug thật trong productlist.js của dự án — nó gọi data.sort() trực tiếp trên mảng đến từ props. Bài 15 sẽ mổ xẻ và sửa. Cách an toàn: [...data].sort(...).

.splice() — xóa/chèn tại vị trí, thay đổi mảng gốc

const arr = ["a", "b", "c"];
arr.splice(1, 1);   // xóa 1 phần tử từ vị trí 1
console.log(arr);   // ["a", "c"]
// redux/slices/cartSlicer.js — xóa sản phẩm khỏi giỏ
const index = cart.findIndex((item) => item._id == product._id && item.size == size);
cart.splice(index, 1);

Bảng so sánh nhanh

Phương thức Trả về Đổi mảng gốc?
.map() Mảng mới cùng độ dài
.filter() Mảng mới ngắn hơn
.find() 1 phần tử / undefined
.findIndex() Số / -1
.reduce() 1 giá trị bất kỳ
.sort() Chính mảng đó
.splice() Mảng các phần tử bị xóa
.push() Độ dài mới

7. == vs ===

1 == "1"    // true  — so sánh lỏng, tự ép kiểu
1 === "1"   // false — so sánh chặt, khác kiểu là false

Quy tắc: luôn dùng ===, trừ khi bạn cố ý muốn ép kiểu.

Trong dự án, code gốc dùng == khá nhiều:

// redux/slices/cartSlicer.js
const index = cart.findIndex((item) => item._id == product._id && item.size == size);

Ở đây == may mắn vẫn đúng vì _id đều là chuỗi. Nhưng viết === sẽ an toàn hơn.

Có một chỗ ==cần thiết: khi so sánh số đến từ input HTML. Input luôn trả về chuỗi, nên "5" === 5false. Cách đúng là ép kiểu tường minh:

Number(e.target.value)   // ✅ Rõ ràng

Dự án đã làm đúng ở chỗ này:

cart[index].quantity = Number(cart[index].quantity) + Number(quantity);

8. Toán tử 3 ngôi và toán tử logic

Toán tử 3 ngôi ? :

const ketQua = dieuKien ? "khi đúng" : "khi sai";

Trong dự án — dùng để đổi class CSS theo trang hiện tại:

// component/navbar.js
<Link className={`nav-link ${patname == "/" ? "active" : " "}`} href="/">

// admin/component/leftbar.js
<Link className={`nav-link rounded-0 ${pathname == "/admin" ? "active" : "text-white"}`}>

Ba ngôi lồng nhau

// cart/page.js — tính đơn giá theo size
product.size === 'l'
  ? (product.price * product.quantity + (product.quantity * 10000)).toLocaleString()
  : product.size === 'm'
    ? (product.price * product.quantity + 15000).toLocaleString()
    : (product.price * product.quantity).toLocaleString()

⚠️ Đoạn này khó đọc và có lỗi logic: size l cộng phụ phí theo số lượng, nhưng size m chỉ cộng một lần cố định. Bài 16 sẽ viết lại cho rõ ràng và nhất quán.

&& — render có điều kiện

// component/product.js
{product.rating > 0 && (
  <small className="text-warning fw-bold me-1">
    {product.rating}<i className="fa-solid fa-star"></i>
  </small>
)}

Đọc là: "nếu rating > 0 thì render phần JSX phía sau, ngược lại không render gì".

⚠️ Bẫy kinh điển: {soLuong && <p>...</p>} — nếu soLuong = 0, React sẽ in ra chữ 0 trên màn hình! Vì 0 là giá trị falsy nhưng vẫn render được. Cách sửa: {soLuong > 0 && <p>...</p>}.


9. Bất đồng bộ: Promise và async/await

Đây là khái niệm quan trọng nhất trong bài này.

Vấn đề

Gọi database hay gọi API mất thời gian (vài mili giây đến vài giây). JavaScript không đứng chờ — nó chạy tiếp dòng dưới. Nên đoạn code sau không hoạt động:

const products = db.collection("products").find().toArray();
console.log(products);   // ❌ In ra: Promise { <pending> } — chưa có dữ liệu!

Promise là gì?

Promise là "lời hứa sẽ có kết quả sau". Nó có 3 trạng thái:

  • pending — đang chờ
  • fulfilled — thành công, có dữ liệu
  • rejected — thất bại, có lỗi

Cách 1 — .then()

fetch("http://localhost:5000/products")
  .then((res) => res.json())      // res chưa phải JSON, phải parse
  .then((data) => console.log(data))
  .catch((err) => console.log("Lỗi:", err));

Dự án dùng cách này ở fetcher và ở nút đặt hàng:

// cart/page.js
fetch(`${process.env.NEXT_PUBLIC_API_URL}/orders`, { method: 'POST', /* ... */ })
  .then(res => {
    alert("đặt hàng thành công");
    dispatch(removeCart());
    router.push("/");
  })

Cách 2 — async/await (dễ đọc hơn nhiều)

async function layProducts() {
  const res = await fetch("http://localhost:5000/products");
  const data = await res.json();
  console.log(data);   // ✅ Có dữ liệu thật
}

Hai quy tắc:

  1. await chỉ dùng được bên trong hàm có async
  2. await làm code "dừng lại chờ" cho đến khi Promise xong

Toàn bộ backend của dự án viết theo kiểu này:

// backend/routes/products.js
router.get("/", async (req, res, next) => {
  const db = await connectDb();                       // chờ kết nối DB
  const productCollection = db.collection("products");
  const products = await productCollection.find().toArray();  // chờ query
  res.status(200).json(products);
});

Bắt lỗi với try/catch

router.get('/products/:id', async (req, res, next) => {
  try {
    const db = await connectDb();
    const product = await db.collection('products').findOne({ _id: new ObjectId(id) });
    res.status(200).json(product);
  } catch (err) {
    next(err);   // chuyển lỗi cho Express xử lý
  }
});

⚠️ Hầu hết route trong dự án gốc KHÔNG có try/catch. Hậu quả: nếu client gửi id sai định dạng (ví dụ /products/id/abc), new ObjectId("abc") sẽ ném lỗi, Express không bắt được và request bị treo vô hạn. Bài 06 sẽ chỉ cách sửa.


10. Module — chia code ra nhiều file

Dự án dùng 2 hệ thống module khác nhau. Đừng nhầm lẫn.

CommonJS — dùng ở backend (Node.js)

// backend/model/db.js — xuất ra
module.exports = connectDb;

// backend/routes/products.js — nhập vào
const connectDb = require("../model/db");
const { ObjectId } = require("mongodb");

ES Modules — dùng ở frontend (Next.js)

// redux/slices/cartSlicer.js — xuất ra
export const { addItem, removeItem } = cartSlice.actions;   // named export
export default cartSlice.reducer;                            // default export

// Nhập vào
import cartSlicer from "./slices/cartSlicer";                // default
import { addItem } from "@/redux/slices/cartSlicer";         // named
import cartSlicer, { addItem } from "./slices/cartSlicer";   // cả hai
Default export Named export
Số lượng mỗi file Tối đa 1 Bao nhiêu cũng được
Cú pháp xuất export default X export const X
Cú pháp nhập import X from "..." import { X } from "..."
Đổi tên khi nhập Tự do Phải dùng as

💡 Có một chỗ trong dự án trộn lẫn 2 kiểu:

// redux/slices/cartSlicer.js
const { createSlice } = require("@reduxjs/toolkit");   // CommonJS
export const cartSlice = createSlice({ ... });         // ES Module

Next.js xử lý được nên vẫn chạy, nhưng không nên viết vậy. Nên dùng nhất quán: import { createSlice } from "@reduxjs/toolkit";

Alias @/ trong Next.js

// frontend/jsconfig.json
{
  "compilerOptions": {
    "paths": { "@/*": ["./*"] }
  }
}

Nhờ cấu hình này, @/redux/store = <thư mục frontend>/redux/store. Bạn không cần đếm ../../../ nữa.


11. Vài hàm tiện ích hay dùng

.toLocaleString() — định dạng số có dấu phân cách

(5050000).toLocaleString();          // "5,050,000"
(5050000).toLocaleString("vi-VN");   // "5.050.000"

Dự án dùng ở nhiều chỗ để hiển thị giá:

<div className="display-1">{product.price.toLocaleString()}đ</div>

⚠️ Có lỗi chính tả trong admin/component/recentOrder.js: viết toLOcaString() (thiếu chữ le, sai hoa thường) → sẽ crash nếu component này được dùng. May mà nó đang bị comment lại trong admin/page.js.

Array(n) + spread — tạo mảng để lặp n lần

// product/[id]/page.js — vẽ số sao đầy
{[...Array(Math.floor(product.rating))].map((i) => (
  <i className="fa-solid fa-star"></i>
))}

// Vẽ số sao rỗng còn lại
{[...Array(5 - Math.floor(product.rating))].map((i) => (
  <i className="fa-regular fa-star"></i>
))}

Giải thích: Array(4) tạo mảng 4 ô rỗng — không .map() được. Phải [...Array(4)] để biến thành [undefined, undefined, undefined, undefined] rồi mới map.

Math.max / Math.min — chặn giá trị

// redux/slices/cartSlicer.js — không cho số lượng nhỏ hơn 1
cart[index].quantity = Math.max(1, quantity);

JSON.stringify / JSON.parse

JSON.stringify({ a: 1 });     // '{"a":1}'  — object → chuỗi
JSON.parse('{"a":1}');        // { a: 1 }   — chuỗi → object

Khi gửi dữ liệu lên server phải chuyển thành chuỗi:

body: JSON.stringify({ user: { fullname, phone, address }, detail: cart })

Object shorthand — viết tắt khi key trùng tên biến

const fullname = "An";
const phone = "0901";

const user = { fullname: fullname, phone: phone };   // dài dòng
const user = { fullname, phone };                     // ✅ tương đương, ngắn gọn

Dùng trong cart/page.js:

body: JSON.stringify({
  user: { fullname, phone, address },   // shorthand
  detail: cart,
  total_money: total,
})

✅ Kiểm tra hiểu bài

Tự viết ra kết quả trước khi chạy thử:

const sp = [
  { ten: "A", gia: 100, sl: 2 },
  { ten: "B", gia: 200, sl: 1 },
  { ten: "C", gia: 50,  sl: 3 },
];

// 1. Tổng tiền toàn bộ?
// 2. Mảng tên các sản phẩm có giá > 80?
// 3. Sản phẩm nào có tên "B"?
// 4. Vị trí của sản phẩm "C"?
// 5. Sắp xếp theo giá tăng dần mà KHÔNG làm đổi mảng gốc?
<details> <summary>Đáp án</summary>
// 1
const tong = sp.reduce((t, p) => t + p.gia * p.sl, 0);   // 100*2 + 200*1 + 50*3 = 550

// 2
const ten = sp.filter((p) => p.gia > 80).map((p) => p.ten);   // ["A", "B"]

// 3
const b = sp.find((p) => p.ten === "B");   // { ten: "B", gia: 200, sl: 1 }

// 4
const i = sp.findIndex((p) => p.ten === "C");   // 2

// 5
const tangDan = [...sp].sort((a, b) => a.gia - b.gia);   // C, A, B
</details>

📝 Bài tập

  1. Viết hàm async function layDanhMuc() gọi https://jsonplaceholder.typicode.com/users và in ra tên tất cả user. Làm cả 2 cách: .then()async/await.

  2. Cho const cart = [{_id:"1", size:"s", quantity:2}, {_id:"1", size:"m", quantity:1}]. Viết hàm thêm sản phẩm {_id:"1", size:"s"} với quantity = 3 sao cho kết quả là quantity của phần tử đầu thành 5 (không thêm phần tử mới).

  3. Giải thích tại sao đoạn này in ra Promise { <pending> }:

    function layData() {
      return fetch("...").then(r => r.json());
    }
    console.log(layData());
    

    Và sửa lại cho đúng.


⬅️ Bài trước | Mục lục | Bài tiếp theo: MongoDB căn bản ➡️


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í