Menu

useLayoutEffect trong React: đo phần tử trước khi vẽ

useLayoutEffect hoạt động giống useEffect nhưng chạy ngay sau khi React cập nhật DOM và trước khi trình duyệt vẽ. Dùng nó để đo một phần tử và chỉnh bố cục mà không bị nháy hình, còn mọi việc khác cứ để useEffect lo.

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.

useLayoutEffect là một phiên bản của useEffect chạy sớm hơn: ngay sau khi React đã cập nhật DOM, nhưng trước khi trình duyệt vẽ khung hình mới. Code trong đó có thể đo các phần tử và thay đổi bố cục, và người dùng chỉ luôn thấy kết quả cuối cùng. Nó chặn việc vẽ trong lúc chạy, nên chỉ dùng khi bạn thật sự cần đúng thời điểm đó.

Các effect được khai báo theo thứ tự effect rồi đến layout effect, nhưng ở mỗi lần render, console luôn in layout effect trước. Thứ tự khai báo giữa hai hook này không quan trọng; thời điểm chạy của chúng mới quan trọng.

Cú pháp

Chữ ký giống hệt useEffect: một hàm setup, một hàm cleanup không bắt buộc và một mảng dependency.

useLayoutEffect(() => {
    // runs after the DOM update, before paint
    return () => {
        // cleanup, before the next run and on unmount
    };
}, [dependencies]);

Mọi điều bạn biết về dependency và cleanup từ useEffect đều áp dụng ở đây. Khác biệt duy nhất là lúc React gọi nó.

Mỗi loại chạy khi nào

Sau một lần render, React cập nhật DOM rồi làm ba việc theo thứ tự cố định:

  1. Chạy đồng bộ tất cả cleanup của layout effect và các layout effect.
  2. Để trình duyệt vẽ. Nếu một layout effect đã set state, React render lại trước, vẫn trước khi vẽ.
  3. Chạy các effect thường, thường là sau khi vẽ.

Trên toàn cây, mọi layout effect chạy trước bất kỳ effect thường nào, và component con chạy trước component cha, vì cha chỉ hoàn tất được khi các con đã vào đúng chỗ:

Console hiện cả hai layout effect trước, Child rồi Parent, sau đó là hai effect thường theo cùng thứ tự. key={n} khiến mỗi lần click mount một Parent mới, nên lần nào bạn cũng thấy đúng bốn dòng đó. Thêm một cleanup có ghi log vào mỗi effect và xem các cleanup cũ chạy trước các effect mới.

Vấn đề nháy hình

Hình dung một tooltip cần hiện phía trên nút của nó, trừ khi phía trên không đủ chỗ, khi đó nó hiện phía dưới. Bạn không thể biết có đủ chỗ hay không cho đến khi tooltip nằm trong DOM và bạn đo được chiều cao của nó. Vì vậy việc render diễn ra hai lượt:

  1. Render tooltip ở đâu đó để có thể đo nó.
  2. Đo nó, tính vị trí thật, set state, render lại.

Với useEffect, trình duyệt có thể vẽ giữa hai lượt đó, và người dùng thấy tooltip nhảy từ vị trí đầu sang vị trí thứ hai. Với useLayoutEffect, lượt thứ hai diễn ra trước khi vẽ, nên vị trí đầu tiên không bao giờ hiện ra.

Rê chuột lên nút trên cùng: phía trên không còn chỗ, nên tooltip hiện phía dưới. Rê lên nút bên dưới thì nó hiện phía trên. Console cho bạn biết phép đo đã chọn bên nào. Đổi useLayoutEffect thành useEffect rồi rê chuột lại: giờ tooltip có thể lóe lên ở đầu khung trong một khung hình trước khi di chuyển. Trên máy nhanh, việc này thường quá nhanh để nhận ra, và đó chính là lý do lỗi này hay lọt lên production rồi bị báo về từ những chiếc điện thoại chậm hơn.

offsetHeight, offsetTop và getBoundingClientRect() đều buộc trình duyệt tính bố cục, nên đọc chúng trong layout effect sẽ cho bạn giá trị thật của DOM mà React vừa ghi ra.

Đo một phần tử

Cách làm này áp dụng cho mọi kích thước mà lần render phụ thuộc vào: đọc nó bằng ref trong một layout effect, lưu vào state, rồi render với giá trị đó.

Click nút và dòng chiều rộng cập nhật trong cùng khung hình với đoạn chữ. Trang không bao giờ hiện chữ mới cạnh chiều rộng cũ, vì lần render thứ hai diễn ra trước khi vẽ. Bỏ [text] khỏi mảng dependency thì phép đo chạy sau mỗi lần render, kể cả lần do chính setWidth của nó gây ra; nó chỉ dừng ở đó vì chiều rộng không còn thay đổi.

Ưu tiên useEffect

Một layout effect giữ việc vẽ lại cho đến khi nó trả về, và nếu nó set state, cho đến khi lần render thêm cũng xong. Một layout effect chậm khiến mọi cập nhật trên trang có cảm giác chậm. Chỉ dùng useLayoutEffect khi tất cả những điều sau đều đúng:

  • Effect đọc thông tin bố cục (kích thước, vị trí, cuộn).
  • Nó thay đổi thứ được render dựa trên giá trị đọc được.
  • Trạng thái trung gian sẽ hiện ra và trông sai.

Mọi thứ khác (lấy dữ liệu, subscription, timer, ghi log, analytics) thuộc về useEffect. Quy tắc nhanh: bắt đầu với useEffect, và chỉ chuyển khi bạn thấy hiện tượng nháy hình.

useEffect thường chạy sau khi vẽ, nhưng không phải lúc nào cũng vậy. Khi cập nhật đến từ một thao tác rời rạc của người dùng như click hay nhấn phím, React có thể chạy các effect trước khi trình duyệt vẽ. Đó là một lý do khiến hiện tượng nháy có thể xuất hiện trong tình huống này mà không xuất hiện trong tình huống khác.

Render phía server

Effect không bao giờ chạy trong lúc render phía server, và useLayoutEffect cũng vậy. Máy chủ gửi HTML của lần render đầu tiên, và layout effect chỉ chạy sau khi hydrate trong trình duyệt. React 18 trở về trước cảnh báo "useLayoutEffect does nothing on the server" khi một component như vậy được render ở máy chủ; React 19 đã bỏ cảnh báo đó.

Nếu khung hình đầu tiên render từ máy chủ trông sai khi chưa có phép đo, có hai cách sửa phổ biến: render một phương án dự phòng trung tính (ví dụ ẩn tooltip cho đến khi đo xong), hoặc chỉ render component ở phía client:

function ClientOnly({ children }) {
    const [mounted, setMounted] = useState(false);
    useEffect(() => setMounted(true), []);
    return mounted ? children : null;
}

useInsertionEffect

Còn một hook effect thứ ba, useInsertionEffect, chạy trước mọi layout effect. Nó dành cho các thư viện CSS-in-JS tạo thẻ <style> lúc runtime: chèn style ở đó nghĩa là một layout effect đo phần tử sẽ thấy nó đã được định kiểu. Hook này không cập nhật được state và ref cũng chưa được gắn, nên nó không có ích gì trong code ứng dụng. Nếu bạn không viết thư viện định kiểu, bạn sẽ không cần đến nó.

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

useEffect và useLayoutEffect khác nhau thế nào?

Ở thời điểm chạy. useLayoutEffect chạy đồng bộ sau khi React đã thay đổi DOM nhưng trước khi trình duyệt vẽ, nên các cập nhật state của nó được áp dụng trước khi người dùng thấy bất cứ thứ gì. useEffect thường chạy sau khi vẽ. API và mảng dependency thì giống nhau.

Khi nào nên dùng useLayoutEffect?

Khi một effect đọc thông tin bố cục (kích thước hay vị trí) rồi lập tức thay đổi thứ được render dựa trên đó, chẳng hạn đặt tooltip ở trên hay dưới nút của nó. Không có nó, người dùng có thể thấy vị trí đầu tiên, sai, trong một khung hình.

useLayoutEffect có làm giảm hiệu năng không?

Nó chặn trình duyệt vẽ cho đến khi chạy xong, cùng với mọi lần render lại mà nó gây ra. Một layout effect chậm khiến cả trang có cảm giác chậm, nên hãy dùng useEffect trừ khi bạn cần tránh hiện tượng nháy hình thấy được.

useLayoutEffect có chạy ở máy chủ không?

Không. Không effect nào chạy trong lúc render phía server. Các phiên bản React cũ in ra cảnh báo khi một component có useLayoutEffect được render ở máy chủ; React 19 không còn làm vậy, nhưng effect vẫn chỉ chạy trong trình duyệt.

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