🔁 Hàm và mảng trong thực tế: map, filter, reduce

By Better Wise

Bài trước bạn đã có arrow function, destructuring và spread, đó chính là nguyên liệu. Bài này ghép chúng lại thành ba hàm mà bạn sẽ dùng gần như mỗi ngày khi viết React: map, filter, reduce.

Sau khoảng 30 phút, bạn sẽ:

  • Giải thích được vì sao React ưu tiên biến đổi mảng thay vì tự thao tác DOM
  • Dùng map để biến đổi từng phần tử và giữ nguyên số lượng
  • Dùng filter để lọc theo điều kiện
  • Dùng reduce để gộp cả mảng về một giá trị
  • Ghép chuỗi ba hàm trên và biết khi nào nên dùng hàm khác

Vì sao React không duyệt mảng bằng vòng lặp DOM

Bạn đã quen đoạn HTML này: một thẻ <ul> rỗng, rồi JavaScript tự tạo từng <li> và gắn vào.

<ul id="ds"></ul>
<script>
  const ds = document.getElementById("ds");
  for (const cv of danhSach) {
    const li = document.createElement("li");
    li.textContent = cv.title;
    ds.appendChild(li);
  }
</script>

Cách này chạy được, nhưng có một cái bẫy: dữ liệu và giao diện là hai nơi phải tự tay giữ đồng bộ. Người dùng bấm "xoá", bạn phải tự tìm đúng <li> để xoá, tự cập nhật số đếm, tự đổi màu khi trạng thái đổi. Quên một chỗ là giao diện lệch với dữ liệu.

React đổi hướng suy nghĩ: dữ liệu là nguồn sự thật duy nhất, giao diện chỉ là kết quả của dữ liệu. Bạn không nói "hãy tạo thẻ li này", bạn nói "với mỗi công việc trong mảng, hãy hiển thị một dòng". Cách mô tả đó chính là map.

Kèm theo một luật ngầm quan trọng: không sửa mảng gốc, luôn tạo _____. push, splice, sort làm thay đổi mảng gốc; [...arr, item] (spread bạn vừa học) thì tạo mảng mới. Lý do sâu xa sẽ rõ ở module 2, giờ chỉ cần nhớ luật.

flowchart TD
  A["📦 Mảng dữ liệu"] --> B["🔍 filter"]
  B --> C["🔄 map"]
  C --> D["🖥️ Giao diện"]
  A --> E["➕ reduce"]
  E --> F["🔢 Một con số / object"]

Dùng chung mảng này cho cả bài, giống dữ liệu thật trong ứng dụng quản lý công việc:

Sign in to view the full content of this lesson. Sign in

const congViec = [
  { id: 1, title: "Học map", done: true, gio: 2 },
  { id: 2, title: "Làm bài tập", done: false, gio: 3 },
  { id: 3, title: "Ôn filter", done: false, gio: 1 },
];

map: biến đổi từng phần tử, giữ nguyên số lượng

map chạy callback trên từng phần tử và luôn trả về _____ với mảng gốc.

const nhan = congViec.map((cv) => cv.title);
// ["Học map", "Làm bài tập", "Ôn filter"]

const nhanKemGio = congViec.map((cv) => `${cv.title} (${cv.gio}h)`);
// ["Học map (2h)", "Làm bài tập (3h)", "Ôn filter (1h)"]

Callback nhận ba tham số, hai tham số sau hay dùng khi cần vị trí:

congViec.map((cv, index) => `${index + 1}. ${cv.title}`);

Lỗi người mới hay mắc nhất: viết map với dấu ngoặc nhọn mà quên return.

congViec.map((cv) => { cv.title });      // [undefined, undefined, undefined]
congViec.map((cv) => { return cv.title; }); // đúng

Warning: map không dùng để "làm gì đó" như in log hay gọi API. Nếu bạn không cần mảng kết quả, hãy dùng forEach. Dùng map chỉ vì tiện là dấu hiệu code đang gây hiểu nhầm cho người đọc sau.

Bài tập 1. Từ congViec, tạo mảng trangThai chứa chuỗi "xong" hoặc "chưa xong".

Xem gợi ý

const trangThai = congViec.map((cv) => (cv.done ? "xong" : "chưa xong"));
// ["xong", "chưa xong", "chưa xong"]

Dùng toán tử ba ngôi bên trong arrow function rút gọn được vì không cần return.

filter: giữ lại phần tử thoả điều kiện

filter trả về mảng mới chỉ gồm những phần tử mà callback trả về _____. Độ dài mảng kết quả có thể nhỏ hơn, kể cả bằng 0.

const chuaXong = congViec.filter((cv) => !cv.done);
// 2 phần tử: "Làm bài tập", "Ôn filter"

const viecNang = congViec.filter((cv) => cv.gio > 1);
// 2 phần tử

Vì callback chỉ cần truthy/falsy, bạn có thể viết gọn congViec.filter((cv) => cv.done). Nhưng cẩn thận với số: filter((cv) => cv.gio) sẽ loại luôn công việc có gio: 0. Khi so sánh, hãy viết rõ điều kiện.

Bài tập 2. Lấy các công việc chưa xong và tốn hơn 1 giờ.

Xem gợi ý

const ketQua = congViec.filter((cv) => !cv.done && cv.gio > 1);
// chỉ còn "Làm bài tập" (3h)

Hai điều kiện nối bằng &&, đọc theo đúng thứ tự bạn sẽ nói bằng lời.

reduce: gộp cả mảng thành một giá trị

reduce nhận hai tham số: hàm xử lý (acc, item) và _____. acc là "cái hộp" được truyền qua từng vòng, mỗi vòng bạn trả về hộp mới.

const tongGio = congViec.reduce((acc, cv) => acc + cv.gio, 0);
// 0 + 2 = 2 -> 5 -> 6

Hình dung bằng đời thường: bạn đi chợ với một cái ví có 0 đồng, mỗi món hàng bạn cộng thêm tiền vào ví. acc là cái ví, 0 là số tiền ban đầu.

Luôn truyền initial value. Bỏ đi thì acc lấy phần tử đầu tiên và kiểu dữ liệu trở nên khó đoán, mảng rỗng thì báo lỗi ngay.

reduce không chỉ để tính tổng. Nó gộp được về object, ví dụ đếm số việc theo trạng thái:

const thongKe = congViec.reduce(
  (acc, cv) => {
    acc[cv.done ? "xong" : "chuaXong"] += 1;
    return acc;
  },
  { xong: 0, chuaXong: 0 }
);
// { xong: 1, chuaXong: 2 }

Bài tập 3. Tính tổng số giờ của các công việc chưa xong.

Xem gợi ý

const tongGioChuaXong = congViec
  .filter((cv) => !cv.done)
  .reduce((acc, cv) => acc + cv.gio, 0);
// 4

Cách khác, gộp trong một lần reduce: acc + (cv.done ? 0 : cv.gio). Cách đầu dễ đọc hơn, và đó thường là lựa chọn đúng.

Ghép chuỗi: filter trước, map sau

Ba hàm đều trả về mảng mới nên ghép nối tiếp được. Thứ tự có ý nghĩa: filter trước thì map chỉ chạy trên phần tử đã lọc, ít việc hơn và ngữ nghĩa rõ hơn.

const nhanViecNang = congViec
  .filter((cv) => cv.gio > 1)
  .map((cv) => cv.title.toUpperCase());
// ["HỌC MAP", "LÀM BÀI TẬP"]

Đến module 2, bạn sẽ thấy đúng chuỗi này nhưng map trả về JSX thay vì chuỗi, và React lo phần render. Cấu trúc suy nghĩ thì y hệt hôm nay.

Vài trường hợp không nên dùng ba hàm này:

Nhu cầu Dùng
Chỉ cần biết có phần tử thoả điều kiện some
Cần tìm một phần tử cụ thể find
Cần dừng giữa chừng for...of
Chỉ cần chạy side effect forEach

Bẫy thường gặp khi dùng map, filter, reduce

  • Quên return trong arrow function có {}. Đây là lỗi số một, kết quả là mảng undefined và bạn sẽ ngồi nhìn màn hình rất lâu.
  • Quên initial value cho reduce. Luôn truyền 0 hoặc {} hoặc [] đúng kiểu bạn muốn nhận về.
  • Sửa mảng gốc bằng push/sort. Hãy tạo bản sao trước: [...arr].sort(...).
  • Dùng reduce cho mọi thứ. Nếu bạn phải đọc lại ba lần mới hiểu, hãy tách thành filter rồi reduce, hoặc viết hàm phụ có tên rõ ràng.
  • Đặt tên biến vô nghĩa. arr2, data1 sẽ giết khả năng đọc code. Hãy đặt viecChuaXong, tongGio.
  • Mẹo debug: console.log sau từng bước của chuỗi, đừng log cả chuỗi dài một lần.

Tóm lại và bước tiếp theo

Ba hàm, ba câu hỏi khác nhau bạn hỏi mảng dữ liệu:

  • map: "biến đổi từng phần tử thành gì?" (giữ nguyên số lượng)
  • filter: "giữ lại phần tử nào?" (có thể ít hơn)
  • reduce: "gộp tất cả thành một giá trị gì?"

Cả ba đều không thay đổi mảng gốc (_____), trả về mảng mới, và ghép chuỗi được. Đó chính là lý do React có thể dựa hoàn toàn vào dữ liệu để quyết định giao diện: bạn mô tả, React tự tính phần DOM cần sửa.

Bài tiếp theo, bạn sẽ dùng chính tư duy này để chia giao diện thành các component và khởi tạo dự án React đầu tiên bằng Vite.

Front: map trả về gì? Back: Mảng mới cùng độ dài, mỗi phần tử là kết quả của callback.

Front: filter trả về gì? Back: Mảng mới chỉ gồm các phần tử mà callback trả về truthy.

Front: reduce dùng để làm gì? Back: Gộp cả mảng về một giá trị duy nhất: số, chuỗi, object hoặc mảng.

Front: Tham số thứ hai của reduce là gì? Back: Initial value, tức giá trị khởi tạo của acc.

Front: Vì sao nên luôn truyền initial value cho reduce? Back: Tránh lỗi với mảng rỗng và tránh kiểu dữ liệu của acc bị đoán sai.

Front: map, filter, reduce có thay đổi mảng gốc không? Back: Không. Chúng trả về mảng mới, giữ nguyên mảng gốc.

Front: Vì sao React dùng map thay vì vòng lặp tạo DOM thủ công? Back: Vì dữ liệu là nguồn sự thật duy nhất, React tự cập nhật DOM khi dữ liệu đổi.

Front: Lỗi gì xảy ra với arr.map(x => { x * 2 })? Back: Thiếu return nên kết quả là mảng gồm toàn undefined.

Front: Muốn kiểm tra có phần tử thoả điều kiện mà không cần mảng mới, dùng gì? Back: Dùng some (hoặc find nếu cần chính phần tử đó).

Front: Tham số thứ hai của callback trong map là gì? Back: index, vị trí của phần tử trong mảng.

Front: Vì sao nên filter trước rồi mới map? Back: map chỉ chạy trên phần tử đã lọc, ít việc hơn và ngữ nghĩa rõ hơn.

Front: Câu hỏi mà filter trả lời là gì? Back: Giữ lại phần tử nào trong mảng.

Front: Khi nào không nên dùng map? Back: Khi chỉ cần chạy side effect, không cần mảng kết quả. Dùng forEach.