React 19 use(): Hướng dẫn toàn diện về Promise và Context

05 tháng 4, 2026·9 phút đọc

API use() trong React 19 thay đổi hoàn toàn cách xử lý dữ liệu bất đồng bộ và Context. Bài viết này sẽ đi sâu vào cơ chế hoạt động, cách tích hợp với Suspense, lợi ích so với useEffect và các mẫu chuyển đổi tối ưu cho lập trình viên.

React 19 use(): Hướng dẫn toàn diện về Promise và Context

React 19, phiên bản ổn định ra mắt vào tháng 12 năm 2024, đã mang đến một trong những bổ sung API quan trọng nhất trong lịch sử của thư viện này: use(). Sau hơn một năm được áp dụng trong hàng ngàn ứng dụng thực tế, use() đã thay đổi căn bản cách các lập trình viên React tiếp cận việc lấy dữ liệu (data fetching), các thao tác bất đồng bộ và tiêu thụ Context.

Trước đây, mô hình chuẩn để lấy dữ liệu trong React thường khá rườm rà: mount thành phần, kích hoạt useEffect, quản lý trạng thái tải và lỗi với useState, và xử lý thủ công việc dọn dẹp cũng như các điều kiện chạy đua (race conditions). Mọi thành phần cần dữ liệu từ máy chủ đều lặp lại một khối mã mẫu (boilerplate) giống hệt nhau. Kết quả là mã nguồn dài dòng, tình trạng "thác nước" (waterfall) trong yêu cầu mạng, nơi các thành phần con phải đợi thành phần cha render xong mới bắt đầu lấy dữ liệu của riêng chúng, cùng trải nghiệm người dùng không đồng nhất.

API use() giải quyết các vấn đề này bằng cách cho phép bạn đọc giá trị của một tài nguyên — cụ thể là một Promise hoặc một Context — trực tiếp trong quá trình render. Khi bạn truyền một Promise vào use(), React sẽ "treo" (suspend) thành phần đó cho đến khi Promise được giải quyết (resolve), ủy quyền trạng thái tải cho ranh giới Suspense gần nhất và các lỗi cho Error Boundary gần nhất. Kết quả là mã nguồn thành phần sạch sẽ hơn nhiều, tách biệt mối quan tâm "tôi cần dữ liệu gì" khỏi "làm thế nào để hiển thị trạng thái tải và lỗi".

Một điểm làm rõ quan trọng trước khi đi sâu hơn: use() về mặt kỹ thuật không phải là một Hook. Đội ngũ React gọi nó là một API. Mặc dù nó trông giống như một Hook và được nhập cùng với các Hook khác từ react, nó không tuân theo các quy tắc của Hook. Bạn có thể gọi use() bên trong các câu lệnh điều kiện, vòng lặp và sau các lệnh trả về sớm (early returns). Đây là một lựa chọn thiết kế có chủ đích khiến nó linh hoạt hơn nhiều so với useState, useEffect và mọi Hook khác trong API của React.

Cách use() hoạt động

Cú pháp

Bề mặt API của use() có vẻ đơn giản một cách đánh lừa:

const value = use(resource);

Tham số resource chấp nhận chính xác hai loại dữ liệu:

  1. Một Promiseuse() sẽ treo thành phần cho đến khi Promise được giải quyết, sau đó trả về giá trị đã resolve.
  2. Một Contextuse() đọc giá trị hiện tại của một React Context, tương tự như useContext().

Đó là toàn bộ API. Không có tùy chọn, không có đối tượng cấu hình, không có generics để xử lý. Sức mạnh đến từ cách use() tích hợp với các cơ chế SuspenseError Boundary hiện có của React.

Tích hợp với Suspense

Khi bạn chuyển một Promise cho use(), React không đợi inline cho Promise đó giải quyết. Thay vào đó, nó "treo" thành phần đó. Việc treo có nghĩa là React ném ra một ngoại lệ nội bộ đặc biệt (bạn sẽ không bao giờ nhìn thấy nó) báo hiệu cho thành phần <Suspense> gần nhất biết rằng nó cần hiển thị trạng thái dự phòng (fallback).

Mô hình này đảo ngược mô hình lấy dữ liệu truyền thống. Thay vì thành phần sở hữu vòng đời fetch (kích hoạt nó, theo dõi trạng thái của nó, dọn dẹp nó), thành phần chỉ đơn giản tuyên bố "tôi cần dữ liệu này" và React xử lý phần còn lại.

Tích hợp với Error Boundary

Nếu Promise được chuyển cho use() bị từ chối (reject), React lan truyền lỗi đó đến Error Boundary gần nhất. Đây là cơ chế tương tự mà React sử dụng cho các lỗi thời gian render, vì vậy nếu bạn đã có Error Boundary trong ứng dụng của mình, chúng sẽ tự động bắt các promise bị từ chối từ use().

use() với Context

Đọc Context bằng use() so với useContext()

use() có thể đọc React Context, đóng vai trò là một giải pháp thay thế cho useContext():

const ThemeContext = createContext("light");

// Với useContext (truyền thống)
function ThemedButtonOld() {
  const theme = useContext(ThemeContext);
  return <button>Nhấn vào tôi</button>;
}

// Với use() (mới)
function ThemedButtonNew() {
  const theme = use(ThemeContext);
  return <button>Nhấn vào tôi</button>;
}

Đối với trường hợp đơn giản này, hai cách tiếp cận về mặt chức năng là giống hệt nhau. Sự khác biệt quan trọng khi bạn cần đọc Context có điều kiện.

Sự khác biệt chính: Đọc Context có điều kiện

Hook useContext phải được gọi ở cấp độ cao nhất của một thành phần. Bạn không thể đặt nó bên trong câu lệnh if, vòng lặp hoặc sau câu lệnh trả về sớm. Đây là quy tắc cơ bản của Hook mà React thực thi.

use() không bị hạn chế này. Vì nó là một API, không phải là Hook, bạn có thể gọi nó một cách có điều kiện:

function AdminPanel() {
  const isAdmin = checkAdminStatus(); // Giả sử hàm này kiểm tra quyền

  if (isAdmin) {
    // use() có thể được gọi bên trong if!
    const adminConfig = use(AdminContext);
    return <Dashboard config={adminConfig} />;
  }

  return <div>Bạn không có quyền truy cập</div>;
}

Chuyển đổi từ useEffect

Hướng dẫn di chuyển từng bước

Dưới đây là cách chuyển đổi một thành phần lấy dữ liệu bằng useEffect sang use():

Bước 1: Trích xuất logic fetch thành một hàm độc trả về một Promise.

Di chuyển lệnh gọi fetch của bạn ra khỏi callback useEffect và vào một hàm thông thường. Đừng await — chỉ cần trả về Promise.

// Trước: fetch bên trong useEffect
useEffect(() => {
  fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(setUser);
}, [id]);

// Sau: hàm độc lập
function fetchUser(id) {
  return fetch(`/api/users/${id}`).then(res => res.json());
}

Bước 2: Tạo Promise ở cấp độ cao hơn.

Thay vì tạo Promise bên trong thành phần (gây ra vòng lặp vô hạn), hãy tạo nó ở thành phần cha hoặc trong Server Component.

// Thành phần cha hoặc Server Component
const userPromise = fetchUser(userId);

// Thành phần con (đọc Promise bằng use())
function UserPage({ userPromise }) {
  const user = use(userPromise);

  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile user={user} />
    </Suspense>
  );
}

Các mẫu nâng cao và các lỗi thường gặp

Luồng dữ liệu (Streaming) với use() và Suspense

Trong kiến trúc Server Components, use() cho phép truyền tải lũy tiến (progressive streaming). Máy chủ có thể bắt đầu gửi HTML cho các thành phần đã giải quyết trong khi vẫn chờ các nguồn dữ liệu chậm hơn:

// Server Component - Next.js App Router
export default async function ArticlePage({ params }) {
  // Giải quyết nhanh - nội dung bài viết từ CDN
  const article = await fetchArticle(params.slug);

  // Chậm hơn - bình luận từ cơ sở dữ liệu
  const commentsPromise = fetchComments(params.slug);

  return (
    <main>
      {/* Phần này hiển thị ngay lập tức với nội dung bài viết */}
      <ArticleContent article={article} />

      {/* Phần này sẽ xuất hiện khi bình luận sẵn sàng */}
      <Suspense fallback={<div>Đang tải bình luận...</div>}>
        <Comments commentsPromise={commentsPromise} />
      </Suspense>
    </main>
  );
}

Lỗi thường gặp: Tạo Promise bên trong thân thành phần

Đây là lỗi số một và nó gây ra việc render lại vô tận:

// LỖI: tạo Promise mới mỗi lần render -> vòng lặp treo vô hạn
function Products() {
  const products = use(fetchProducts()); // fetchProducts() tạo Promise mới
  return <div>{products.map(...)}</div>;
}

Cách khắc phục: Tạo Promise trong thành phần cha, bộ nạp tuyến đường (route loader), Server Component hoặc bọc nó trong useMemo.

// SỬA: Sử dụng useMemo
function Products() {
  const productsPromise = useMemo(() => fetchProducts(), []);
  const products = use(productsPromise);
  return <div>{products.map(...)}</div>;
}

Sử dụng use() cho Đăng ký (Subscriptions)

use() được thiết kế để đọc một lần (one-time read) một Promise hoặc Context. Nó không phải là cơ chế đăng ký. Nếu bạn cần phản ứng với dữ liệu thay đổi theo thời gian (tin nhắn WebSocket, cập nhật cơ sở dữ liệu thời gian thực), hãy sử dụng useEffect kết hợp useState, hoặc thư viện useSyncExternalStore.

// SAI: use() chỉ đọc giá trị ban đầu
function LivePrice({ symbol }) {
  const price = use(fetchPrice(symbol)); 
  return <span>{price}</span>;
}

// ĐÚNG: useEffect + useState cho đăng ký
function LivePrice({ symbol }) {
  const [price, setPrice] = useState(null);
  useEffect(() => {
    const ws = new WebSocket(`wss://prices.example.com/${symbol}`);
    ws.onmessage = (event) => setPrice(JSON.parse(event.data).price);
    return () => ws.close();
  }, [symbol]);
  return <span>{price ?? "Đang tải..."}</span>;
}

Kết luận

API use() trong React 19 đại diện cho một sự thay đổi thực sự trong cách các ứng dụng React xử lý dữ liệu bất đồng bộ và Context. Bằng cách tích hợp với Suspense và Error Boundaries, nó loại bỏ các mã mẫu thừa thãi (boilerplate) đã ám ảnh việc lấy dữ liệu trong React trong nhiều năm và cung cấp một mô hình khai báo (declarative model), nơi các thành phần mô tả dữ liệu chúng cần thay vì cách lấy dữ liệu đó.

Những điểm chính cần nhớ:

  • use() đọc Promises và Context trong quá trình render. Nó là một API, không phải là Hook, và có thể được gọi trong các câu lệnh điều kiện và vòng lặp.
  • Promises phải ổn định. Tạo chúng trong Server Components, route loaders hoặc useMemo — không bao giờ bên trong thân render của thành phần gọi use().
  • Suspense xử lý tải, Error Boundaries xử lý lỗi. Sự tách biệt mối quan tâm này giữ cho mã thành phần sạch sẽ và chuyển việc quản lý trạng thái UI sang cấu trúc cây thành phần.
  • use() bổ sung, không thay thế hệ sinh thái hiện có. useEffect vẫn là công cụ đúng cho các đăng ký và tác dụng phụ. use() là công cụ đúng cho các lần đọc dữ liệu một lần tại thời điểm render.

Nếu bạn đang bắt đầu một dự án React 19 mới, hãy biến use() thành phương pháp mặc định để lấy dữ liệu của bạn. Kết quả sẽ là mã nguồn sạch hơn, nhanh hơn và dễ bảo trì hơn.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗