Đ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

React 19 có gì mới? Server Components, hooks mới và cách tối ưu app web 2026

Ếch Trendy
Ếch Trendy

2 tháng trước · 9 phút đọc

Tại sao React 19 khác những lần nâng cấp trước?

Mình nhớ hồi React 16 ra đời với Hooks - cảm giác như cả cộng đồng dừng lại để học lại từ đầu. React 19 không gây ra cơn địa chấn kiểu đó, nhưng mình nghĩ nó quan trọng hơn nhiều người đang nghĩ.

Lý do? React 18 vẫn cơ bản là client-side library với một số tính năng concurrent. React 19 thì khác - đây là bước chuyển rõ ràng sang mô hình server-first, action-driven. Server Components và Server Functions đã chính thức ổn định, không còn là experimental nữa. Actions được chuẩn hóa thành pattern chính thức cho mọi thao tác bất đồng bộ. React Compiler bắt đầu trưởng thành.

346814 React 19 đánh dấu bước chuyển từ client-first sang server-first rendering

Nếu bạn đang build web app trên Next.js, Remix, hay bất kỳ meta-framework nào hỗ trợ RSC - đây là thời điểm cần hiểu rõ hướng đi mới. Bài này mình sẽ đi qua từng thay đổi quan trọng, kèm code thực tế và quan điểm cá nhân về khi nào nên (và không nên) áp dụng.


Server Components vs Client Components: quy tắc thực dụng

Đây là phần mà mình thấy nhiều bạn vẫn còn mơ hồ nhất. Không phải vì khái niệm khó, mà vì không có ranh giới rõ ràng trong code - bạn phải tự biết mình đang ở layer nào.

Quy tắc đơn giản nhất: mặc định để Server Component, chỉ chuyển sang Client Component khi thật sự cần interactivity phía browser.

346815 Ranh giới server/client không phải về performance đơn thuần - là về trách nhiệm của từng layer

Cụ thể hơn:

Dùng Server Component khi:

  • Fetch dữ liệu từ database, CMS, API nội bộ
  • Đọc filesystem, environment variables, secrets
  • Render layout, marketing pages, content-heavy pages
  • Muốn giảm JavaScript bundle gửi xuống client

Chuyển sang Client Component khi:

  • Component cần useState, useEffect, event handlers
  • Cần đọc window, localStorage, media query
  • Drag-and-drop, modal, tabs, rich text editor
  • Animation phụ thuộc vào tương tác người dùng
// Server Component - mặc định, không cần directive
async function ProductList() {
  // Đọc DB trực tiếp, không expose ra client
  const products = await db.query('SELECT * FROM products');
  
  return (
    <ul>
      {products.map(p => (
        // Client Component chỉ ở layer mỏng nhất
        <li key={p.id}>
          <span>{p.name}</span>
          <AddToCartButton productId={p.id} /> {/* Client Component */}
        </li>
      ))}
    </ul>
  );
}
// Client Component - chỉ thêm 'use client' khi cần
'use client';

import { useState } from 'react';

export function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false);
  
  return (
    <button onClick={() => setAdded(true)}>
      {added ? 'Đã thêm ✓' : 'Thêm vào giỏ'}
    </button>
  );
}

Một mistake mình thấy thường xuyên: bọc cả page trong 'use client' vì có một button tương tác. Điều này phủ nhận toàn bộ lợi ích của Server Components. Cách đúng là tách button thành Client Component riêng, còn phần còn lại của trang vẫn là Server Component.


Actions và form hooks: trước/sau rõ nhất trong React 19

Đây là thay đổi mình thấy giá trị nhất cho developer hằng ngày. Nếu bạn đã từng tự viết loading state, error state, reset form sau submit - bạn sẽ biết nó tẻ nhạt thế nào.

React 19 chuẩn hóa pattern này qua ba hook: useActionState, useFormStatus, và useOptimistic.

Trước React 19

// Tự quản lý pending, error, reset - boilerplate dài
function CommentForm() {
  const [text, setText] = useState('');
  const [pending, startTransition] = useTransition();
  const [error, setError] = useState(null);

  async function onSubmit(e) {
    e.preventDefault();
    setError(null);
    startTransition(async () => {
      try {
        await fetch('/api/comments', {
          method: 'POST',
          body: JSON.stringify({ text }),
        });
        setText(''); // Tự reset
      } catch (err) {
        setError('Có lỗi xảy ra'); // Tự xử lý lỗi
      }
    });
  }

  return (
    <form onSubmit={onSubmit}>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button disabled={pending}>
        {pending ? 'Đang gửi...' : 'Gửi'}
      </button>
      {error && <p className="error">{error}</p>}
    </form>
  );
}

Sau React 19

'use client';

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';

// Action tách ra ngoài - dễ test, dễ reuse
async function createComment(prevState, formData) {
  const text = formData.get('text');
  
  try {
    await fetch('/api/comments', {
      method: 'POST',
      body: JSON.stringify({ text }),
    });
    return { message: 'Đã đăng bình luận!', error: null };
  } catch {
    return { message: '', error: 'Có lỗi xảy ra, thử lại nhé' };
  }
}

// Submit button là component riêng - đọc trạng thái từ form cha
function SubmitButton() {
  const { pending } = useFormStatus(); // Phải nằm TRONG <form>
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Đang gửi...' : 'Gửi bình luận'}
    </button>
  );
}

export default function CommentForm() {
  const [state, formAction] = useActionState(createComment, {
    message: '',
    error: null,
  });

  return (
    <form action={formAction}>
      <textarea name="text" placeholder="Nhập bình luận..." />
      <SubmitButton />
      {state.error && <p className="error">{state.error}</p>}
      {state.message && <p className="success">{state.message}</p>}
    </form>
  );
}

346816 useFormStatus phải nằm trong component con, không gọi trực tiếp ở form cha

Một điểm cần chú ý: useFormStatus đọc trạng thái của form cha gần nhất trong DOM. Nếu bạn gọi hook này trực tiếp trong component chứa <form>, nó sẽ không hoạt động như mong đợi. Phải tách submit button thành component riêng nằm bên trong <form> - như ví dụ SubmitButton ở trên.

Và đừng nhầm useActionState với useFormState - cái sau đã được đổi tên trong bản ổn định. Nếu code của bạn vẫn dùng useFormState, kiểm tra lại docs của version đang dùng.


useOptimistic: UI mượt mà không cần đợi server

Bạn có bao giờ click like trên một post rồi nhìn màn hình đứng im chờ server phản hồi? Trải nghiệm đó tệ hơn nhiều so với ngay lập tức thấy số like tăng lên - dù thật ra server vẫn đang xử lý.

useOptimistic giải quyết đúng vấn đề này. Hook cho phép hiển thị UI "dự đoán" ngay lập tức, React sẽ tự rollback nếu action thất bại.

'use client';

import { useOptimistic, useTransition } from 'react';

async function toggleLike(postId) {
  const res = await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
  return res.json();
}

export function LikeButton({ postId, initialLikes, initialLiked }) {
  const [isPending, startTransition] = useTransition();
  
  // optimisticState là UI hiện tại người dùng thấy
  const [optimisticLiked, setOptimisticLiked] = useOptimistic(
    { liked: initialLiked, count: initialLikes },
    (current, action) => ({
      liked: action === 'like',
      count: action === 'like' ? current.count + 1 : current.count - 1,
    })
  );

  function handleClick() {
    startTransition(async () => {
      // Cập nhật UI ngay lập tức
      setOptimisticLiked(optimisticLiked.liked ? 'unlike' : 'like');
      // Gọi server song song
      await toggleLike(postId);
    });
  }

  return (
    <button onClick={handleClick} disabled={isPending}>
      {optimisticLiked.liked ? '❤️' : '🤍'} {optimisticLiked.count}
    </button>
  );
}

346817 Optimistic UI: người dùng thấy kết quả ngay, server xử lý ngầm phía sau

Pattern này đặc biệt hiệu quả cho: like/dislike, checkbox todo, thêm item vào giỏ hàng, gửi tin nhắn chat. Bất kỳ action nào người dùng expect kết quả gần như tức thì.

Trade-off cần biết: nếu server thất bại, UI sẽ rollback về trạng thái trước đó. Điều này tạo ra một "flash" khó chịu nếu lỗi xảy ra thường xuyên. Nên dùng useOptimistic cho các action có xác suất thành công cao, và luôn hiển thị error message rõ ràng khi rollback.


React Compiler: bạn có cần xóa useMemo không?

Câu hỏi mình hay thấy trên mạng sau khi React Compiler được công bố: "Giờ tôi có thể xóa hết useMemo và useCallback không?"

Câu trả lời ngắn: chưa nên làm vội.

React Compiler phân tích code của bạn và tự động thêm memoization ở những nơi cần thiết - tương tự như khi bạn tự viết useMemo, nhưng compiler làm thay. Mục tiêu là giảm "optimization noise" trong codebase: developer viết code logic, compiler lo performance.

346818 React Compiler tự động hóa memoization - nhưng cần rollout dần, không phải bật cả codebase ngay

Lợi ích thực tế cho team lớn:

  • Code dễ đọc hơn, ít wrapping trong useCallback
  • Ít bug do memoization dependency array sai
  • Junior dev không cần hiểu sâu về memoization để viết code performant

Nhưng có một số điều cần thực tế:

1. Rollout dần, đừng bật toàn bộ codebase ngay. Compiler có thể handle 90% trường hợp, nhưng 10% còn lại - thường là những pattern phức tạp hoặc side effects không rõ ràng - có thể gây ra behavior bất ngờ.

2. Đo trước khi xóa. Mình khuyên bạn chạy React DevTools Profiler để xem render count trước và sau khi bật compiler trên một route cụ thể. Chỉ xóa manual memoization khi bạn đã xác nhận compiler đang xử lý đúng.

3. Compiler không thay thế tư duy. Bạn vẫn cần hiểu khi nào computation đắt, khi nào nên tách component, khi nào dependency thay đổi không cần thiết. Compiler hỗ trợ, không thay thế.

Cách bật compiler trong Next.js 15:

// next.config.js
const nextConfig = {
  experimental: {
    reactCompiler: true, // Bật compiler
  },
};

module.exports = nextConfig;

Sau khi bật, chạy profiling và so sánh kỹ trước khi remove useMemo / useCallback có sẵn.


Setup Next.js 15 với React 19

Nếu bạn đang bắt đầu dự án mới, đây là cách mình khuyến nghị setup. Với dự án hiện có, phần migrate bên dưới sẽ phù hợp hơn.

Tạo project mới

# Tạo Next.js 15 app với App Router
npx create-next-app@latest my-app \
  --typescript \
  --tailwind \
  --eslint \
  --app

cd my-app

# Kiểm tra version
npm list react react-dom next

Next.js 15 khi dùng App Router sẽ mặc định hỗ trợ React 19 và Server Components. Bạn không cần cài thêm gì - chỉ cần hiểu rõ convention:

app/
  layout.tsx        → Server Component (mặc định)
  page.tsx          → Server Component (mặc định)
  components/
    server/         → Server Components (không directive)
    client/         → Client Components ('use client' ở đầu file)

Cấu hình TypeScript cho Server Actions

// lib/actions.ts - Server Actions phải có 'use server'
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;
  
  // Gọi DB trực tiếp - code này chạy trên server
  await db.insert({ title, content });
  
  // Invalidate cache sau mutation
  revalidatePath('/posts');
  
  return { success: true };
}
// app/posts/new/page.tsx - Server Component dùng Server Action
import { createPost } from '@/lib/actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Tiêu đề" />
      <textarea name="content" placeholder="Nội dung" />
      <button type="submit">Đăng bài</button>
    </form>
  );
}

346819 App Router: Server Components làm data layer, Client Components chỉ ở layer tương tác

Lưu ý khi kiểm tra compatibility: một số thư viện UI phổ biến (component library, form library) chưa hoàn toàn support React 19. Kiểm tra peerDependencies của từng package trước khi cài. Nếu thấy cảnh báo peer dependency conflict, đọc changelog của library đó - thường họ đã có version mới tương thích.

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