Menu

Hook use trong React: đọc promise và context khi render

use đọc giá trị của một promise hoặc một context trong lúc render. Với promise, nó tạm dừng (suspend) cho đến khi dữ liệu về và hiển thị fallback của Suspense gần nhất. Khác với các hook khác, nó có thể chạy bên trong câu lệnh if và vòng lặp.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

use là một API của React 19, đọc giá trị từ một promise hoặc một context trong lúc component render. use(promise) tạm dừng component cho đến khi promise resolve, trong lúc chờ thì hiển thị fallback của <Suspense> gần nhất; use(context) hoạt động như useContext nhưng có thể nằm trong một câu lệnh if hoặc một vòng lặp.

Hàm fetchUser dưới đây đóng vai một request thật.

Lần render đầu hiển thị "Loading..." trong một giây, rồi đến hồ sơ người dùng. Click User 2: fallback quay lại trong lúc promise mới đang chờ. Profile không có trạng thái loading riêng; nó được viết như thể dữ liệu đã có sẵn.

use(promise) hoạt động thế nào

Khi Profile gọi use(userPromise):

  • Nếu promise đã resolve, use trả về giá trị của nó và quá trình render tiếp tục.
  • Nếu promise vẫn đang chờ, React dừng render Profile và hiển thị fallback của <Suspense> gần nhất phía trên. Khi promise xong, React render Profile lại, và lần này use trả về giá trị.
  • Nếu promise bị reject, React hiển thị error boundary gần nhất.

React ghi nhớ kết quả ngay trên chính object promise. Đó là lý do promise phải là cùng một object ở lần render tiếp theo.

Để giữ hồ sơ cũ trên màn hình trong lúc hồ sơ mới đang tải, hãy đặt promise bên trong một transition. Khi đó React sẽ chờ thay vì hiển thị lại fallback:

<button onClick={() => startTransition(() => setUserPromise(fetchUser(2)))}>User 2</button>

Nên tạo promise ở đâu

Đây là phần khiến nhiều người vấp. Viết lời gọi fetch ngay trong component đọc dữ liệu trông rất tự nhiên nhưng không chạy được:

// Do not do this
function Profile({ id }) {
    const user = use(fetchUser(id)); // a new promise on every render
    return <p>{user.name}</p>;
}

Mỗi lần render đều gọi fetchUser và nhận một promise mới đang chờ. Profile tạm dừng. Khi promise đó resolve, React render Profile lại, việc này tạo ra một promise mới đang chờ, và component lại tạm dừng. Component không bao giờ hiển thị được dữ liệu, còn tab Network thì đầy request.

Hãy tạo promise ở nơi không chạy lại sau mỗi lần render của component đọc nó:

  • Trong component cha, giữ trong state (như ví dụ đầu tiên) hoặc tạo trong một trình xử lý sự kiện.
  • Ở cấp module, khi dữ liệu không phụ thuộc vào props: const configPromise = fetchConfig(); phía trên component.
  • Trong một cache có khóa là các đối số, để cùng một id trả về cùng một promise.
  • Trong một framework: Server Component có thể truyền promise cho client component qua prop, và route loader cũng làm tương tự. Đây là trường hợp mà use được thiết kế cho.

Đây là phiên bản dùng cache, để component có thể yêu cầu dữ liệu theo id mà vẫn nhận được một promise ổn định:

Click lần lượt ba thành phố, rồi quay lại một thành phố bạn đã mở. Console ghi một request cho mỗi thành phố, và thành phố bạn đã xem hiện ra ngay mà không qua fallback, vì promise của nó đã resolve. Xóa điều kiện if (!cache.has(city)) và component lại quay về tình trạng tạm dừng mãi mãi.

Một cache thật còn cần cách để làm hết hạn các mục. Thư viện dữ liệu và framework lo việc đó, nên phần lớn ứng dụng lấy promise từ một trong số chúng.

use(context), kể cả trong if

use(SomeContext) trả về cùng giá trị với useContext(SomeContext). Khác biệt nằm ở chỗ bạn có thể gọi nó ở đâu. Mọi hook khác phải chạy ở cấp cao nhất, theo cùng thứ tự ở mỗi lần render (quy tắc của hook). use có thể được gọi sau một lệnh return sớm, bên trong một điều kiện hoặc trong vòng lặp.

Click nút để chuyển giữa hai nhánh. Đổi use(UserContext) thành useContext(UserContext) thì code vi phạm quy tắc của hook, vì hook sẽ chạy ở một số lần render và không chạy ở những lần khác.

use vẫn có giới hạn: chỉ gọi nó trong lúc render component hoặc bên trong một custom hook. Trong trình xử lý sự kiện hay effect, hãy đọc context bằng giá trị bạn đã lấy được trong lúc render.

Lỗi: error boundary, không phải try/catch

Khi promise bị reject, lỗi được chuyển tới error boundary gần nhất, giống như một lỗi khi render.

Mở Post 2 và error boundary hiển thị thông báo lỗi. Mở lại Post 1 thì mọi thứ chạy bình thường: key={id} cho error boundary một state mới với mỗi bài viết, nên nó không tiếp tục hiển thị lỗi cũ.

Bạn không thể bắt một use đang chờ hoặc bị reject bằng try/catch. Khi promise đang chờ, use ngắt lần render bằng cách ném ra một giá trị đặc biệt, và một khối catch bao quanh sẽ nuốt mất giá trị đó. Giá trị bị bắt là một lỗi có thông báo bắt đầu bằng "Suspense Exception: This is not a real error!", và ở development React còn ghi log "use was called from inside a try/catch block". Để hiển thị một giá trị dự phòng thay vì màn hình lỗi, hãy xử lý việc reject ngay trên promise trước khi truyền nó xuống:

const postPromise = fetchPost(id).catch(() => 'This post could not be loaded.');

So sánh use với fetch trong useEffect

Mẫu kinh điển phía client fetch trong một effect và giữ ba phần state:

function Profile({ id }) {
    const [user, setUser] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        let ignore = false;
        fetchUser(id).then(
            (u) => !ignore && setUser(u),
            (e) => !ignore && setError(e),
        );
        return () => {
            ignore = true;
        };
    }, [id]);

    if (error) return <p>Error</p>;
    if (!user) return <p>Loading...</p>;
    return <p>{user.name}</p>;
}
useEffectuse
Khi nào request bắt đầusau khi lần render đầu đã lên màn hìnhkhi promise được tạo, có thể trước cả khi component render
Trạng thái loadingif (!user) do bạn tự viết<Suspense> gần nhất
Lỗistate error do bạn tự quản lýerror boundary gần nhất
Race conditionbạn tự chặn (ignore)component luôn đọc promise mà nó được giao
Ai tạo promisechính componentcomponent cha, cache hoặc framework

Phiên bản dùng effect tự chứa mọi thứ, nên nó vẫn là cách phổ biến nhất trong các ứng dụng không dùng framework; trang fetch dữ liệu trình bày đầy đủ về nó. use phát huy thế mạnh khi một thứ gì đó phía trên component bắt đầu request từ sớm và truyền promise xuống.

Câu hỏi thường gặp

Hook use trong React dùng để làm gì?

use(resource) trả về giá trị của một promise hoặc một context. Với promise, component tạm dừng cho đến khi promise resolve và <Suspense> gần nhất hiển thị fallback của nó; nếu promise bị reject, error boundary gần nhất sẽ hiển thị thay thế.

Có thể gọi use theo điều kiện không?

Có. use là hook duy nhất bạn có thể gọi bên trong câu lệnh if, vòng lặp và sau một lệnh return sớm. Nó vẫn phải được gọi từ một component hoặc một hook, không bao giờ từ trình xử lý sự kiện hay effect.

Vì sao use(fetch()) trong component bị lặp mãi?

Mỗi lần render tạo ra một promise mới. Component tạm dừng chờ nó, React render lại khi nó resolve, và lần render đó lại tạo một promise mới đang chờ. Hãy tạo promise bên ngoài lần render: trong state của component cha, ở cấp module, trong cache, hoặc trong loader của framework.

Có thể bọc use trong try/catch không?

Không. Khi promise đang chờ, use ngắt lần render bằng cách ném ra một giá trị, và bắt giá trị đó sẽ làm hỏng cơ chế này. Hãy xử lý promise bị reject bằng error boundary, hoặc gắn .catch() vào promise để nó resolve thành một giá trị dự phòng.

Có nên thay việc fetch dữ liệu bằng useEffect bằng use không?

use cần một promise được tạo một lần và dùng lại, đó là thứ mà framework và thư viện dữ liệu cung cấp cho bạn. Trong một ứng dụng phía client thuần, fetch trong effect (hoặc với thư viện như TanStack Query) vẫn phổ biến và hoàn toàn ổn.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu