JavaScript array methods hiện đại: dùng đúng map, reduce, toSorted, groupBy và những method ít người biết
Khi nào flatMap thay được map + filter, vì sao sort() làm hỏng React state và xếp sai số lẫn tên tiếng Việt, cách dùng toSorted, with, Object.groupBy, structuredClone, Set methods và iterator helpers ES2025.
Ba bug mình gặp trong cùng một dashboard quản lý đơn hàng. Cột "Tổng tiền" sort ra 100.000 < 25.000. Danh sách khách hàng có "Đặng Văn Bình" nằm tận cuối, sau cả chữ V. Bấm nút sort thì bảng không đổi gì, phải bấm thêm nút khác mới thấy cập nhật. Cả ba đều đến từ một method: Array.prototype.sort.
Array API của JavaScript đã thay đổi đáng kể từ ES2022 đến ES2025: có at(), findLast(), nhóm method không mutate (toSorted, with...), Object.groupBy, Set methods và iterator helpers. Phần lớn chạy sẵn trên Node.js 22+ và trình duyệt hiện đại, nhưng code mình review vẫn đầy [...arr].sort(), reduce với spread, và JSON.parse(JSON.stringify()).
Bài này đi qua từng nhóm method với dữ liệu đơn hàng thật, chỉ ra khi nào nên dùng, nó tốn gì và edge case nào hay cắn người. Mọi ví dụ đã chạy trên Node.js 24 và type-check với TypeScript lib: ["ESNext", "DOM"].
TL;DR:
flatMapgộp được map + filter trong một lượt.reducehợp để cộng dồn, nhưng dùng để dựng object bằng spread là O(n²).sort()mặc định so sánh chuỗi và mutate mảng gốc. DùngtoSorted(comparator). Với tên tiếng Việt, dùngIntl.Collator('vi')tạo một lần rồi tái sử dụng.toSorted,toReversed,toSpliced,with(ES2023) trả mảng mới, hợp với React state, nhưng chỉ là shallow copy.Object.groupBy/Map.groupBy(ES2024) thay choreducegom nhóm. Set methods và iterator helpers (ES2025) đã Baseline, Node.js 22+ có sẵn.
Dữ liệu dùng xuyên suốt bài:
type OrderStatus = 'pending' | 'paid' | 'refunded'
type OrderLine = { sku: string; quantity: number; unitPrice: number }
type Order = { id: string; customerId: string; status: OrderStatus; createdAt: string; lines: OrderLine[] }
const orders: Order[] = [
{ id: 'DH-1001', customerId: 'c1', status: 'paid', createdAt: '2026-10-02T08:00:00Z',
lines: [{ sku: 'AT-01', quantity: 2, unitPrice: 150_000 }] },
{ id: 'DH-1002', customerId: 'c2', status: 'pending', createdAt: '2026-10-15T09:30:00Z',
lines: [{ sku: 'QJ-07', quantity: 1, unitPrice: 420_000 }] },
{ id: 'DH-1003', customerId: 'c1', status: 'refunded', createdAt: '2026-11-01T14:10:00Z',
lines: [{ sku: 'AT-01', quantity: 1, unitPrice: 150_000 }, { sku: 'NB-02', quantity: 1, unitPrice: 90_000 }] },
]
const orderTotal = (o: Order) => o.lines.reduce((sum, l) => sum + l.quantity * l.unitPrice, 0)
map, filter, flatMap và khi nào reduce làm code khó đọc
// map + filter: 2 lượt duyệt, 1 mảng trung gian
const paidTotals = orders
.filter((o) => o.status === 'paid')
.map((o) => ({ id: o.id, total: orderTotal(o) }))
// flatMap: trả [] để loại bỏ, [x] để giữ lại. Một lượt, không có mảng trung gian
const paidTotals2 = orders.flatMap((o) => (o.status === 'paid' ? [{ id: o.id, total: orderTotal(o) }] : []))
// flatMap để "trải phẳng" quan hệ 1-nhiều
const soldSkus = orders.flatMap((o) => o.lines.map((l) => l.sku))
// ['AT-01', 'QJ-07', 'AT-01', 'NB-02']
flatMap chỉ làm phẳng một cấp. Callback trả về [[x]] thì bạn nhận [x] lồng trong mảng kết quả. Cần làm phẳng sâu hơn thì dùng flat(depth) hoặc flat(Infinity). Về đọc hiểu, filter().map() vẫn dễ đọc hơn khi điều kiện và phép biến đổi tách bạch. Mình chỉ chuyển sang flatMap khi điều kiện phụ thuộc vào kết quả biến đổi (parse thành công mới giữ) hoặc khi mảng đủ lớn để mảng trung gian đáng kể.
reduce rất hợp để cộng dồn ra một giá trị như orderTotal ở trên. Nó bắt đầu gây hại khi bị dùng để dựng object hay mảng:
// ❌ O(n²): mỗi vòng lặp copy toàn bộ object tích lũy
const byIdSlow = orders.reduce<Record<string, Order>>((acc, o) => ({ ...acc, [o.id]: o }), {})
// ✅ O(n), đọc là hiểu ngay
const byId = new Map(orders.map((o) => [o.id, o]))
const byIdObject = Object.fromEntries(orders.map((o) => [o.id, o]))
Với 10 phần tử thì không ai thấy khác biệt. Với 50.000 dòng import từ file Excel, bản spread copy tổng cộng hàng trăm triệu thuộc tính và có thể treo tab vài giây. Quy tắc của mình: nếu callback của reduce dài hơn 3 dòng hoặc có spread accumulator, viết lại bằng for...of, Map, Object.fromEntries hoặc Object.groupBy. Ngoài ra luôn truyền giá trị khởi tạo cho reduce: gọi trên mảng rỗng mà không có initial value sẽ ném TypeError.
Tìm kiếm: find, findLast, some, every, includes và at
type StatusEvent = { orderId: string; status: 'created' | 'paid' | 'shipped' | 'refunded'; at: string }
const events: StatusEvent[] = [
{ orderId: 'DH-1001', status: 'created', at: '2026-10-02T08:00:00Z' },
{ orderId: 'DH-1001', status: 'paid', at: '2026-10-02T08:05:00Z' },
{ orderId: 'DH-1001', status: 'shipped', at: '2026-10-03T10:00:00Z' },
]
events.find((e) => e.status === 'paid') // phần tử đầu tiên khớp, hoặc undefined
events.findLast((e) => e.orderId === 'DH-1001') // trạng thái mới nhất: 'shipped'
events.findIndex((e) => e.status === 'shipped') // 2
events.findLastIndex((e) => e.status === 'refunded') // -1: không có
events.some((e) => e.status === 'refunded') // false, dừng ngay khi gặp phần tử khớp
events.every((e) => e.orderId === 'DH-1001') // true, dừng ngay khi gặp phần tử không khớp
events.at(-1)?.status // 'shipped'. Mảng rỗng → undefined, không ném lỗi
findLast/findLastIndex (ES2023) thay cho [...events].reverse().find(), vốn tạo một bản copy chỉ để tìm từ cuối. Với log trạng thái được append theo thời gian, phần tử cần tìm thường nằm gần cuối nên findLast còn dừng sớm hơn.
some/every short-circuit: callback ngừng chạy ngay khi biết kết quả. Đừng đặt side effect trong callback của chúng (gửi log, đếm) vì bạn không biết nó chạy bao nhiêu lần. Edge case nguy hiểm nhất là [].every(...) luôn trả true. Nếu viết cartItems.every((i) => i.paid) để quyết định hiện nút "Đã thanh toán", giỏ rỗng sẽ hiện là đã thanh toán. Luôn kiểm tra items.length > 0 && items.every(...).
includes và indexOf khác nhau ở NaN: [NaN].includes(NaN) là true vì includes dùng thuật toán SameValueZero, còn [NaN].indexOf(NaN) là -1 vì nó dùng ===. Khi chỉ cần trả lời "có hay không", dùng includes. Còn at(-1) thay cho arr[arr.length - 1], nhận chỉ số âm, chạy được trên cả string và TypedArray.
toSorted, toReversed, toSpliced, with: không mutate và React state
sort, reverse, splice sửa trực tiếp mảng gốc. Trong React, đây là nguồn của bug "bấm mà UI không cập nhật":
import { useState } from 'react'
type Row = { id: string; customerName: string; total: number; status: 'pending' | 'paid' }
export function OrdersTable({ initialRows }: { initialRows: Row[] }) {
const [rows, setRows] = useState(initialRows)
function sortByTotalBad() {
// ❌ sort() mutate mảng và trả về CHÍNH mảng đó: cùng reference nên React bỏ qua update,
// đồng thời làm hỏng initialRows của component cha
setRows(rows.sort((a, b) => b.total - a.total))
}
function sortByTotal() {
setRows((prev) => prev.toSorted((a, b) => b.total - a.total)) // ✅ mảng mới
}
function markPaid(index: number) {
setRows((prev) => prev.with(index, { ...prev[index], status: 'paid' })) // thay 1 phần tử
}
function removeRow(index: number) {
setRows((prev) => prev.toSpliced(index, 1)) // xóa 1 phần tử, có thể chèn thêm như splice
}
return (
<table>
<thead>
<tr>
<th><button onClick={sortByTotal}>Tổng tiền</button></th>
<th><button onClick={sortByTotalBad}>Tổng tiền (bản lỗi)</button></th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={row.id}>
<td>{row.customerName}</td>
<td>{row.total.toLocaleString('vi-VN')} đ</td>
<td>
{row.status === 'pending' && <button onClick={() => markPaid(i)}>Đánh dấu đã trả</button>}
<button onClick={() => removeRow(i)}>Xóa</button>
</td>
</tr>
))}
</tbody>
</table>
)
}
React so sánh state mới với state cũ bằng Object.is. rows.sort() trả về đúng reference cũ nên React coi như không có gì thay đổi. Lần sau có state khác đổi, bảng mới đột ngột hiện thứ tự mới, và đó là lý do bug "phải bấm nút khác mới thấy". toSorted (cùng toReversed, toSpliced, with) luôn trả về mảng mới, bỏ được thói quen [...rows].sort().
Ba edge case cần nhớ:
- Đây là shallow copy.
rows.toSorted()[0].status = 'paid'vẫn sửa object nằm trong mảng gốc. Muốn cập nhật phần tử thì tạo object mới nhưmarkPaidở trên. with(index, value)némRangeErrorkhi index nằm ngoài mảng, khác với gánarr[10] = xvốn âm thầm tạo "lỗ". Index âm được tính từ cuối.- Các method này biến lỗ (hole) trong sparse array thành
undefined:[, 1].toSorted()cho ra[1, undefined]. TypedArray cótoSorted,toReversed,withnhưng không cótoSpliced.
Sort comparator: số, ngày và tên tiếng Việt
const amounts = [10, 1, 5, 100, 25]
amounts.toSorted() // [1, 10, 100, 25, 5]: mặc định so sánh CHUỖI theo UTF-16
amounts.toSorted((a, b) => a - b) // [1, 5, 10, 25, 100]
// Ngày dạng ISO 8601: so sánh theo timestamp, mới nhất lên đầu
orders.toSorted((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))
// Nhiều khóa: theo trạng thái, cùng trạng thái thì tổng tiền giảm dần
orders.toSorted((a, b) => a.status.localeCompare(b.status) || orderTotal(b) - orderTotal(a))
Comparator phải trả về số âm, 0 hoặc dương. Viết (a, b) => a > b trả boolean: true thành 1, false thành 0, không bao giờ âm. Trên V8, [5, 1, 4].sort((a, b) => a > b) trả về nguyên [5, 1, 4]. TypeScript bắt lỗi này ngay lúc compile, JavaScript thì không. Từ ES2019, sort được đảm bảo stable: các phần tử bằng nhau giữ nguyên thứ tự ban đầu, nên bạn có thể sort lần lượt theo từng khóa, hoặc dùng || như trên.
Giờ đến tên tiếng Việt:
const customers = ['Đặng Văn Bình', 'Nguyễn Thị An', 'Trần Minh Anh', 'Lê Văn Ánh', 'Phạm Thu Ân',
'Hoàng Ăn', 'Vũ Đức Dũng', 'Bùi Văn Đạt', 'Ngô Văn Duy']
customers.toSorted()
// [..., 'Vũ Đức Dũng', 'Đặng Văn Bình']: "Đ" là U+0110, đứng sau mọi chữ Latin cơ bản
const viCollator = new Intl.Collator('vi') // tạo MỘT lần, tái sử dụng
customers.toSorted(viCollator.compare)
// ['Bùi Văn Đạt', 'Đặng Văn Bình', 'Hoàng Ăn', 'Lê Văn Ánh', 'Ngô Văn Duy',
// 'Nguyễn Thị An', 'Phạm Thu Ân', 'Trần Minh Anh', 'Vũ Đức Dũng']
// Danh sách lớp/nhân sự ở Việt Nam thường xếp theo TÊN (từ cuối), rồi mới đến họ đệm
const givenName = (fullName: string) => fullName.trim().split(/\s+/).at(-1) ?? ''
const keyed = customers.map((name) => ({ name, key: givenName(name) })) // tính khóa 1 lần/phần tử
keyed
.toSorted((a, b) => viCollator.compare(a.key, b.key) || viCollator.compare(a.name, b.name))
.map((x) => x.name)
// ['Nguyễn Thị An', 'Trần Minh Anh', 'Lê Văn Ánh', 'Hoàng Ăn', 'Phạm Thu Ân',
// 'Đặng Văn Bình', 'Vũ Đức Dũng', 'Ngô Văn Duy', 'Bùi Văn Đạt']
// Số trong chuỗi: "Đơn 2" trước "Đơn 10"
;['Đơn 10', 'Đơn 2', 'Đơn 1'].toSorted(new Intl.Collator('vi', { numeric: true }).compare)
// ['Đơn 1', 'Đơn 2', 'Đơn 10']
Output trên là kết quả chạy trên Node.js 24 (bản build có full ICU, mặc định từ Node 13). Collation tiếng Việt coi ă và â là chữ cái riêng đứng sau a, đ là chữ cái riêng đứng sau d, còn dấu thanh chỉ là khác biệt cấp hai. Vì vậy "Anh" đứng trước "Ánh", "Ánh" lại đứng trước "Ăn", và "Duy" đứng trước "Đạt".
Hai lưu ý hiệu năng. a.localeCompare(b, 'vi') phải dựng hoặc tra collator ở mỗi lần so sánh, trong khi sort gọi comparator khoảng n·log n lần, nên với mảng lớn hãy tạo Intl.Collator một lần như trên. Tương tự, đừng gọi givenName() trong comparator: tính khóa trước (decorate–sort–undecorate) để mỗi phần tử chỉ bị split một lần.
Khi tìm kiếm "không dấu", đừng dựa vào sensitivity: 'base'. Nó coi "Ánh" bằng "anh" nhưng "Đạt" vẫn khác "dat", vì với collation tiếng Việt, đ là chữ cái riêng. Hãy chuẩn hóa chuỗi:
const removeTones = (s: string) =>
s.normalize('NFD').replace(/\p{Diacritic}/gu, '').replace(/đ/g, 'd').replace(/Đ/g, 'D')
removeTones('Bùi Văn Đạt').toLowerCase().includes('dat') // true
Object.groupBy và Map.groupBy (ES2024)
const byStatus = Object.groupBy(orders, (o) => o.status)
// { paid: [DH-1001], pending: [DH-1002], refunded: [DH-1003] }
byStatus.paid?.length ?? 0 // nhóm không có phần tử nào thì key không tồn tại
Object.hasOwn(byStatus, 'refunded') // true. Không có byStatus.hasOwnProperty: object có null prototype
// Map.groupBy: key là giá trị bất kỳ, giữ thứ tự chèn, duyệt bằng for...of
const byMonth = Map.groupBy(orders, (o) => o.createdAt.slice(0, 7))
for (const [month, list] of byMonth) {
console.log(month, list.length) // '2026-10' 2, rồi '2026-11' 1
}
// Key là object: gom đơn theo chính object khách hàng
type Customer = { id: string; name: string; tier: 'gold' | 'standard' }
const customersById = new Map<string, Customer>([
['c1', { id: 'c1', name: 'Nguyễn Thị An', tier: 'gold' }],
['c2', { id: 'c2', name: 'Bùi Văn Đạt', tier: 'standard' }],
])
const byCustomer = Map.groupBy(orders, (o) => customersById.get(o.customerId)!)
Kiểu trả về của Object.groupBy trong TypeScript là Partial<Record<K, T[]>>. Đó chính là lời nhắc rằng mỗi nhóm có thể undefined, nên luôn dùng ?. hoặc ?? []. Object trả về có prototype null, nên các lời gọi kiểu byStatus.hasOwnProperty('paid') ném TypeError. Dùng Object.hasOwn hoặc in.
Chọn Map.groupBy khi key không phải string (số, Date bucket, object) hoặc khi cần giữ thứ tự nhóm theo lần xuất hiện. Nhớ rằng key object so sánh theo reference: hai object { id: 'c1' } khác nhau là hai nhóm khác nhau. Cả hai method có sẵn từ Node.js 21 và trên các trình duyệt hiện đại. Với TypeScript, cần lib là ES2024 hoặc ESNext.
Array.from, Array.from({ length }) và structuredClone
// Array.from(iterable, mapFn): chuyển + biến đổi trong 1 bước, không tạo mảng trung gian
const checkedSkus = Array.from(
document.querySelectorAll<HTMLInputElement>('input[name="sku"]:checked'),
(input) => input.value,
)
// Array.from({ length }): tạo dãy
const months = Array.from({ length: 12 }, (_, i) => `Tháng ${i + 1}`)
const pageNumbers = Array.from({ length: Math.ceil(237 / 20) }, (_, i) => i + 1) // [1..12]
// ❌ fill với object/array: mọi ô trỏ tới CÙNG một mảng
const bucketsBad = new Array(3).fill([])
bucketsBad[0].push('DH-1001') // [['DH-1001'], ['DH-1001'], ['DH-1001']]
// ✅ mỗi ô là một mảng riêng
const buckets = Array.from({ length: 3 }, () => [] as string[])
// Loại trùng
const uniqueSkus = Array.from(new Set(soldSkus)) // ['AT-01', 'QJ-07', 'NB-02']
structuredClone là cách deep copy chuẩn, có sẵn trên trình duyệt hiện đại và Node.js 17+:
const draft = structuredClone(orders[0]) // copy sâu: lines là mảng mới, object mới
draft.lines[0].quantity = 5
console.log(orders[0].lines[0].quantity) // 2: bản gốc không bị ảnh hưởng
// Giữ được Date, Map, Set, ArrayBuffer, RegExp và cả tham chiếu vòng
const snapshot = structuredClone({ exportedAt: new Date(), totals: new Map([['DH-1001', 300_000]]) })
// JSON round-trip thì không: Date → string, Map → {}, undefined bị bỏ
JSON.parse(JSON.stringify({ exportedAt: new Date(), note: undefined })) // { exportedAt: '2026-...' }
class Money {
constructor(public amount: number, public currency: 'VND' | 'USD') {}
format() {
return `${this.amount.toLocaleString('vi-VN')} ${this.currency}`
}
}
structuredClone(new Money(150_000, 'VND')) instanceof Money // false: mất prototype, chỉ còn data
// structuredClone({ onPaid: () => {} }) → DOMException DataCloneError: function không clone được
Hãy coi structuredClone là công cụ copy dữ liệu: không clone được function, DOM node, và instance của class sẽ biến thành plain object. Nó cũng không rẻ. Với React state, bạn hầu như không cần deep copy, chỉ cần copy đúng nhánh mình sửa (with, spread). Dùng structuredClone cho bản nháp của form chỉnh sửa, snapshot để undo, hay dữ liệu gửi qua postMessage.
Set methods và iterator helpers (ES2025)
const required = new Set(['orders:read', 'orders:refund'])
const granted = new Set(['orders:read', 'invoices:read'])
required.difference(granted) // Set { 'orders:refund' }: quyền còn thiếu
required.isSubsetOf(granted) // false
granted.intersection(required) // Set { 'orders:read' }
granted.union(new Set(['reports:read'])) // Set { 'orders:read', 'invoices:read', 'reports:read' }
required.symmetricDifference(granted) // có ở đúng một bên
required.isDisjointFrom(new Set(['admin:*'])) // true
// Tham số phải là "set-like" (có size, has, keys): Map được, Array thì không
// required.union(['admin:*']) → TypeError: The .size property is NaN
Trước đây bạn phải viết [...a].filter((x) => !b.has(x)) cho phép hiệu. Bây giờ ý đồ nằm ngay trong tên method, và kết quả là Set mới, không mutate cái nào. Kiểm tra quyền, so sánh tag, diff danh sách feature flag giữa hai môi trường là những chỗ dùng ngay được.
Iterator helpers mang map, filter, take, drop, flatMap, reduce, some, every, find, toArray lên mọi iterator, và chúng chạy lazy:
// Lấy 5 mã đơn hoàn tiền đầu tiên mà không tạo mảng trung gian nào
const firstRefundIds = orders
.values() // Array Iterator
.filter((o) => o.status === 'refunded')
.map((o) => o.id)
.take(5) // đủ 5 phần tử là dừng duyệt
.toArray()
// Generator vô hạn + take
function* invoiceNumbers(start: number) {
for (let n = start; ; n++) yield `INV-${String(n).padStart(6, '0')}`
}
invoiceNumbers(1024).take(3).toArray() // ['INV-001024', 'INV-001025', 'INV-001026']
// Iterator.from() bọc iterator của thư viện cũ để dùng được các helper này
Với mảng một triệu đơn, orders.filter(...).map(...).slice(0, 5) duyệt hết một triệu phần tử và tạo hai mảng lớn. Bản iterator dừng ngay khi đủ 5 kết quả. Đổi lại, iterator chỉ dùng được một lần, không có length, không truy cập theo index. Trong vòng lặp cực nóng, một for loop thuần vẫn có thể nhanh hơn, nên hãy đo trên dữ liệu của bạn.
Hỗ trợ runtime: Set methods đạt Baseline từ tháng 6/2024, iterator helpers từ tháng 3/2025 (theo MDN). Node.js 22+ có sẵn cả hai. Nếu vẫn phải hỗ trợ trình duyệt cũ hơn, dùng polyfill của core-js hoặc kiểm tra trên MDN. Với TypeScript, type có trong lib: ["ESNext"]: Set methods từ TS 5.5, iterator helpers từ TS 5.6.
Mutating vs non-mutating và hiệu năng với mảng lớn
| Mutate mảng gốc | Phiên bản không mutate | Ghi chú |
|---|---|---|
sort(fn) |
toSorted(fn) (ES2023) |
Cả hai stable từ ES2019 |
reverse() |
toReversed() (ES2023) |
|
splice(i, n, ...items) |
toSpliced(i, n, ...items) (ES2023) |
splice trả phần tử bị xóa, toSpliced trả mảng mới |
arr[i] = x |
with(i, x) (ES2023) |
with ném RangeError nếu index ngoài mảng |
push, unshift |
[...arr, x], arr.concat(x) |
|
pop, shift |
arr.slice(0, -1), arr.slice(1) |
|
fill, copyWithin |
Array.from({ length }, fn) |
|
| (không mutate) | map, filter, flatMap, slice, concat, flat |
Luôn trả mảng mới, shallow |
Khi mảng lên tới hàng trăm nghìn hoặc hàng triệu phần tử, mấy điều sau quan trọng hơn chọn method nào "đẹp":
- Mảng trung gian: mỗi bước
filter,map,slicetrong chuỗi tạo ra một mảng mới. Một vòngfor...ofhoặc iterator helpers giảm áp lực bộ nhớ và GC. includestrong vòng lặp là O(n·m). Kiểm tra 10.000 SKU trong danh sách 50.000 SKU thì chuyển danh sách thànhSettrước,hasgần như O(1).shift()trên mảng lớn là O(n) vì phải dời mọi phần tử. Dùng con trỏ index nếu đang làm hàng đợi.Math.max(...arr)truyền từng phần tử làm argument. Trên Node.js 24, mình chạy thử với mảng 500.000 phần tử và nhậnRangeError: Maximum call stack size exceeded. Với mảng lớn, dùng vòng lặp hoặcreduce.toSorted,toSpliced,withcopy cả mảng mỗi lần gọi. Gọi trong vòng lặp là O(n²) bộ nhớ, khi đó copy một lần rồi mutate bản copy cục bộ.- Comparator chạy khoảng n·log n lần: tính khóa trước, tái sử dụng
Intl.Collator.
Đừng tin số benchmark trên mạng, kể cả của mình. Đo bằng performance.now() hoặc một thư viện benchmark như tinybench/mitata, trên dữ liệu có kích thước và hình dạng giống production.
Những lỗi thường gặp
// ❌ forEach không chờ async callback: hàm "xong" trước khi đơn nào được gửi
async function syncOrdersBad(ids: string[]) {
ids.forEach(async (id) => {
await fetch(`/api/orders/${id}/sync`, { method: 'POST' })
})
console.log('Đã đồng bộ') // in ra ngay lập tức
}
// ✅ Song song có chờ (cân nhắc giới hạn concurrency nếu ids lớn)
async function syncOrders(ids: string[]) {
const results = await Promise.allSettled(ids.map((id) => fetch(`/api/orders/${id}/sync`, { method: 'POST' })))
const failed = results.filter((r) => r.status === 'rejected').length
console.log(`Đồng bộ xong, ${failed} lỗi`)
}
// ❌ map(parseInt): parseInt nhận (value, index) và coi index là radix
;['1', '2', '3'].map(parseInt) // [1, NaN, NaN]
// ✅
;['1', '2', '3'].map(Number) // [1, 2, 3]
;['1', '2', '3'].map((s) => Number.parseInt(s, 10))
// ❌ every trên giỏ rỗng trả true
const canCheckoutBad = (items: { inStock: boolean }[]) => items.every((i) => i.inStock)
// ✅
const canCheckout = (items: { inStock: boolean }[]) => items.length > 0 && items.every((i) => i.inStock)
// ❌ Sort trực tiếp props hoặc state
// props.orders.sort(...) → mutate dữ liệu của component cha
// ✅ props.orders.toSorted(...)
// ❌ Xóa phần tử bằng delete: để lại "lỗ", length không đổi
const skus = ['AT-01', 'QJ-07', 'NB-02']
delete skus[1] // ['AT-01', <1 empty item>, 'NB-02']
// ✅
const remaining = skus.toSpliced(1, 1)
Checklist
- Tìm mọi
.sort(trong codebase: thêm comparator cho mảng số, đổi sangtoSortednếu mảng là props/state. - Danh sách tên hoặc chuỗi tiếng Việt sort bằng một
Intl.Collator('vi')dùng chung, khóa sort tính trước. - Thay
reducecó spread accumulator bằngMap,Object.fromEntrieshoặcObject.groupBy. - Mọi chỗ dùng
everyđể bật tắt tính năng đều kiểm tra mảng rỗng trước. - Thay
JSON.parse(JSON.stringify(x))bằngstructuredClone(x)khi dữ liệu có Date/Map/Set. - Kiểm tra quyền và diff tập hợp bằng Set methods. Xác nhận target runtime (Node.js 22+, trình duyệt Baseline 2024) hoặc thêm polyfill.
- Với pipeline xử lý mảng lớn hơn 100.000 phần tử, đo trước rồi mới chọn giữa chuỗi method, iterator helpers và
forloop. - Bật
lib: ["ESNext", "DOM"](hoặc target phù hợp) trongtsconfigđể TypeScript nhận các method mới.