Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

JavaScript ES2025: Các tính năng và patterns dev web cần biết

Ếch Trendy
Ếch Trendy

5 tháng trước · 10 phút đọc

ES2025 là gì - và tại sao bạn không nên học theo checklist

Mình từng mắc cái bẫy này: cứ mỗi năm có ECMAScript mới là lên Google tìm "ES20XX features" rồi đọc hết, nhớ syntax, xong... không biết dùng vào đâu.

Vấn đề là JavaScript không có bước nhảy lớn như ngày xưa ES6 (2015). Từ đó đến nay, TC39 - tổ chức quản lý chuẩn ECMAScript - ra mắt theo từng năm, mỗi năm bundle các proposal đã đạt Stage 4 vào bản chính thức. "ES2025" là tên gọi của edition mới nhất trong chu kỳ này.

344556 Proposal đạt Stage 4 mới vào chuẩn - đừng học Stage 2/3 vội

Thực tế với dev web thì khác hơn lý thuyết một chút: browser, Node.js, và TypeScript thường ship tính năng trước khi edition chính thức được đặt tên. Nên câu hỏi quan trọng hơn là: tính năng này đã chạy được ở runtime mình đang dùng chưa?

Bài này mình sẽ không đi theo kiểu checklist. Thay vào đó, mình chọn những thứ thực sự thay đổi cách viết code trong React/Next.js - cùng với patterns đi kèm để code dễ bảo trì hơn.


Iterator Helpers - lười biếng theo đúng nghĩa

"Lười biếng" (lazy) ở đây là compliment.

Trước ES2025, nếu muốn xử lý một mảng lớn qua nhiều bước - filter, transform, take top 10 - bạn phải tạo mảng trung gian ở mỗi bước. 10.000 phần tử filter còn 500, rồi map ra 500 object mới, rồi slice 10. Ba lần cấp phát bộ nhớ.

Iterator Helpers giải quyết điều này bằng cách chain các thao tác trực tiếp trên iterator, chỉ thực thi khi thực sự cần:

// Giả sử apiData là một iterator lớn từ generator hoặc lazy source
const result = apiData
  .filter(item => item.active)
  .map(item => ({ id: item.id, label: item.name.trim() }))
  .take(10)
  .toArray();

Không có mảng trung gian nào được tạo ra cho đến dòng .toArray(). Các method phổ biến bạn sẽ dùng:

  • .filter() - lọc theo điều kiện
  • .map() - biến đổi từng phần tử
  • .take(n) - lấy n phần tử đầu
  • .drop(n) - bỏ qua n phần tử đầu
  • .flatMap() - map rồi flatten
  • .toArray() - thu thập kết quả
  • .forEach() - duyệt không thu thập
  • .some() / .every() / .find() - kiểm tra điều kiện
  • .reduce() - tổng hợp

344557 Iterator lazy - không tạo mảng trung gian, chỉ tính khi cần

Dùng trong React thực tế: Khi fetch API trả về danh sách sản phẩm lớn, bạn cần filter theo category và chỉ render 20 sản phẩm đầu tiên. Thay vì:

// Cách cũ - 3 mảng trung gian
const filtered = products.filter(p => p.category === activeTab);
const transformed = filtered.map(p => toViewModel(p));
const visible = transformed.slice(0, 20);

Với Iterator Helpers:

// products cần là iterator - dùng Array.prototype[Symbol.iterator]() hoặc generator
const visible = products.values()
  .filter(p => p.category === activeTab)
  .map(p => toViewModel(p))
  .take(20)
  .toArray();

Trade-off cần biết: Iterator Helpers hoạt động với iterator object, không phải array trực tiếp. Bạn cần gọi .values() hoặc dùng generator. Với mảng nhỏ dưới vài trăm phần tử, chain Array methods truyền thống vẫn ổn và dễ đọc hơn. Dùng Iterator Helpers khi data source thực sự lớn hoặc có thể lazy (stream, generator).


Explicit Resource Management - dọn dẹp theo kiểu người lớn

Bao nhiêu lần bạn viết code kiểu này và quên cleanup?

// Mở kết nối, xử lý, nhớ đóng
const db = await openConnection();
try {
  const data = await db.query('SELECT ...');
  // xử lý...
} finally {
  await db.close(); // Quên dòng này = memory leak
}

Explicit Resource Management (hay còn gọi là using declarations) giải quyết vấn đề cleanup bị quên. Đây là pattern mượn từ C# using và Python with:

// Symbol.dispose cho cleanup đồng bộ
function makeDbConnection() {
  const conn = openConnection();
  return {
    ...conn,
    [Symbol.dispose]() {
      conn.close();
      console.log('Đã đóng kết nối');
    }
  };
}

// Dùng với 'using' - tự động gọi dispose khi ra khỏi scope
{
  using conn = makeDbConnection();
  const data = conn.query('SELECT ...');
  // Khi block này kết thúc, conn[Symbol.dispose]() tự gọi
  // Dù có throw error hay không
}

Với async, dùng Symbol.asyncDispose và await using:

async function processFile() {
  await using handle = await openFileHandle('./data.json');
  const content = await handle.read();
  return JSON.parse(content);
  // handle tự đóng khi function return
}

344558 using tự cleanup khi ra scope - không còn lo quên finally

Trong Next.js server code: Pattern này cực kỳ hữu ích cho Route Handlers và Server Actions khi làm việc với DB connection, file handles, hoặc distributed locks:

// app/api/report/route.ts
export async function GET() {
  await using db = await getDbConnection();
  await using lock = await acquireLock('report-generation');
  
  const data = await db.query('SELECT ...');
  return Response.json(data);
  // db và lock tự release - kể cả khi có lỗi
}

Trade-off: using declarations cần TypeScript 5.2+ hoặc transpilation. Trong React component, useEffect cleanup vẫn là cách chuẩn cho UI lifecycle - using phù hợp hơn cho non-UI resources ở server-side và utility layers. Đừng cố nhét using vào mọi chỗ.


Patterns đi kèm - phần quan trọng hơn cú pháp

Sự thật là: tính năng ngôn ngữ mới chỉ có giá trị khi bạn biết cách ghép chúng vào workflow thực tế. Dưới đây là các patterns mình thấy quan trọng nhất khi build React/Next.js với JavaScript hiện đại.

Pure functions cho data transforms

Tách logic xử lý data ra khỏi component. Hoàn toàn. Component chỉ lo render:

// utils/product.js - pure functions, không side effect
export const toViewModel = (product) => ({
  id: product.id,
  label: product.name.trim(),
  priceText: `${product.price.toLocaleString('vi-VN')}đ`,
  inStock: product.quantity > 0,
});

export const filterByCategory = (products, category) =>
  products.filter(p => p.category === category);

// Component chỉ gọi hàm, không tự xử lý
function ProductList({ products, activeCategory }) {
  const viewModels = filterByCategory(products, activeCategory)
    .map(toViewModel);
  
  return viewModels.map(vm => <ProductCard key={vm.id} {...vm} />);
}

Lợi ích? Test pure functions không cần mock React, không cần render. Một file test đơn giản là đủ.

Immutable state updates

React detect change bằng reference. Mutate trực tiếp = bug khó tìm:

// ❌ React không biết state đã thay đổi
state.items.push(newItem);
setState(state);

// ✅ Spread để tạo object mới
setState(prev => ({
  ...prev,
  items: [...prev.items, newItem],
}));

// ✅ Với object nested dùng structuredClone hoặc Immer
import { produce } from 'immer';
setState(produce(draft => {
  draft.items.push(newItem); // OK vì Immer handle immutability
}));

344559 Patterns quyết định maintainability về lâu dài hơn syntax mới

Typed contracts với TypeScript + Zod

Khi API trả về data, đừng tin blindly. Validate ở runtime:

import { z } from 'zod';

// Định nghĩa schema
const ProductSchema = z.object({
  id: z.string(),
  name: z.string().min(1),
  price: z.number().positive(),
  category: z.enum(['electronics', 'clothing', 'food']),
});

type Product = z.infer<typeof ProductSchema>;

// Trong Server Action hoặc API route
export async function fetchProducts(): Promise<Product[]> {
  const raw = await api.get('/products');
  return z.array(ProductSchema).parse(raw); // Throw nếu data sai shape
}

Zod schema vừa là runtime validation, vừa là TypeScript type - không cần viết hai lần.

Nếu bạn muốn nắm JavaScript vững hơn để áp dụng các pattern này, khóa Lập Trình JavaScript Nâng Cao trên F8 cover khá sâu về closure, reference types và cách JS thực sự hoạt động - foundation tốt trước khi đi vào ES2025.


Áp dụng trong Next.js App Router

Next.js App Router (từ Next.js 13+) mở ra ranh giới rõ ràng giữa Server Components và Client Components. JavaScript ES2025-era features phát huy tốt nhất ở đâu trong kiến trúc này?

Server Components là nơi an toàn nhất để dùng tính năng mới nhất. Runtime Node.js trên server luôn mới hơn browser, không cần lo polyfill:

// app/dashboard/page.tsx - Server Component
import { getDashboardData } from '@/lib/data';
import { transformForChart } from '@/lib/transforms';

export default async function DashboardPage() {
  const raw = await getDashboardData();
  
  // Xử lý data trực tiếp trên server, không ship logic này xuống client
  const chartData = raw.metrics.values()
    .filter(m => m.date >= thirtyDaysAgo)
    .map(m => ({ x: m.date, y: m.value }))
    .toArray();
  
  return <Chart data={chartData} />;
}

Server Actions với Explicit Resource Management:

// app/actions/export.ts
'use server';

export async function exportReport(filters: ReportFilters) {
  await using db = await getReadonlyConnection();
  
  const data = await db.query(buildQuery(filters));
  const csv = toCsvString(data); // pure function
  
  return { content: csv, filename: `report-${Date.now()}.csv` };
  // db tự đóng
}

344560 Server vs Client boundary - dùng tính năng mới thoải mái hơn ở server

Client Components cần cẩn thận hơn với browser support. Kiểm tra MDN compatibility trước khi dùng tính năng mới ở đây, hoặc để transpiler (Babel/SWC) lo:

// app/components/FilterPanel.tsx
'use client';

// Iterator Helpers ở client - cần verify browser support
// Hoặc fallback về Array methods truyền thống cho safety
function FilterPanel({ items }: { items: Product[] }) {
  const [activeFilter, setActiveFilter] = useState('all');
  
  // An toàn hơn: dùng Array methods ở client
  const filtered = items
    .filter(item => activeFilter === 'all' || item.category === activeFilter)
    .slice(0, 20);
    
  return <div>...</div>;
}

Rule đơn giản: Server = dùng thoải mái. Client = verify support hoặc để transpiler lo.

Nếu bạn đang học React từ đầu hoặc muốn có nền tảng vững cho App Router, khóa Xây Dựng Website với ReactJS cover từ cơ bản đến nâng cao, phù hợp trước khi đi sâu vào Next.js.


Ecosystem đi kèm - đừng dùng mình JavaScript thuần

Modern JS features mạnh hơn khi kết hợp đúng thư viện. Đây là những gì mình thực sự dùng trong production:

Thư viện Dùng cho Ghi chú
TypeScript Type safety, refactor an toàn Bắt buộc với team lớn
Zod Runtime validation, schema-first types Thay thế joi/yup cho TypeScript projects
React Hook Form Form state + validation Tích hợp tốt với Zod qua resolver
TanStack Query Server state caching, retry, sync Giảm boilerplate fetch/loading/error
Immer Immutable updates dễ đọc Đặc biệt hữu ích với state phức tạp nested
Vitest Test pure functions, utility code Nhanh hơn Jest, tích hợp tốt với Vite/Next.js

344561 Bộ công cụ này giúp JS hiện đại phát huy tối đa trong React/Next.js

Một điểm quan trọng về Lodash: nhiều helper của Lodash giờ không cần nữa nhờ native JS đã đủ mạnh. _.map, _.filter, _.find, _.flatten - tất cả đã có native equivalent. Chỉ giữ Lodash khi bạn cần _.debounce, _.throttle, _.cloneDeep, hay các utility phức tạp mà native chưa cover.

// Không cần Lodash cho các thao tác cơ bản
const result = items
  .filter(Boolean)           // thay _.compact
  .map(x => x.value)         // thay _.map
  .flat();                   // thay _.flatten

// Vẫn nên dùng Lodash cho
import { debounce } from 'lodash-es';
const handleSearch = debounce(search, 300);

Dùng lodash-es thay vì lodash nếu bạn dùng ES modules - bundle size nhỏ hơn đáng kể nhờ tree-shaking.

Takeaways:

  • Iterator Helpers giúp xử lý data lớn không cần mảng trung gian - dùng ở server hoặc generator pipeline
  • Explicit Resource Management (using) giúp cleanup tự động, phù hợp cho server-side utilities
  • Patterns như pure functions + immutable updates + typed contracts quan trọng hơn việc biết từng tính năng mới
  • Server Components là nơi an toàn nhất để thử nghiệm ES2025 features
  • Ecosystem (TypeScript, Zod, TanStack Query) nhân đôi giá trị của modern JS

Bước tiếp theo thực tế: chọn một dự án đang có, refactor một module data-processing theo pure functions + typed schema. Không cần rewrite toàn bộ - cải thiện từng phần nhỏ. Comment cho mình biết bạn gặp khó khăn ở bước nào nhé.

Trang web dùng cookie để hoạt động và đo hiệu quả quảng cáo. Xem chính sách bảo mật