Hydration failed: Khi UI 'khát nước' và không đồng bộ - Debug từ đâu?

Hydration failed: Khi UI 'khát nước' và không đồng bộ - Debug từ đâu?

Chào anh em lập trình! Nếu bạn đang làm việc với các framework JavaScript hiện đại như React, Next.js, Nuxt.js hay SvelteKit và đã từng "đứng hình" trước thông báo lỗi "Hydration failed because the initial UI does not match what was rendered on the server." thì xin chúc mừng... bạn không đơn độc! Lỗi này khá phổ biến, nhưng cũng có những cách debug rất hiệu quả để giải quyết nó. Hãy cùng tôi "mổ xẻ" xem Hydration là gì và chúng ta sẽ bắt đầu debug từ đâu nhé!

Hydration là gì và tại sao lại "khát nước"?

Hãy hình dung thế này: khi bạn truy cập một trang web được xây dựng bằng Server-Side Rendering (SSR) hoặc Static Site Generation (SSG), server sẽ gửi về một file HTML đã có sẵn nội dung. Đây là lý do trang web của bạn tải nhanh và SEO tốt. Sau đó, trình duyệt sẽ tải về các file JavaScript và "gắn" các sự kiện, trạng thái, và làm cho UI trở nên tương tác được. Quá trình "gắn" JavaScript vào HTML tĩnh này chính là Hydration (tạm dịch là "tưới nước").

Lỗi "Hydration failed" xảy ra khi JavaScript ở phía client cố gắng "tưới nước" cho một cây DOM (Document Object Model) mà nó cho rằng đã được render trên server, NHƯNG lại phát hiện ra rằng cấu trúc hoặc nội dung của cây DOM đó KHÔNG khớp với những gì nó mong đợi. Giống như bạn mang một cái bình hoa ra tưới, nhưng cái bình lại không phải loại bạn nghĩ vậy!

Các bước khoanh vùng và xử lý lỗi Hydration failed

Để giải quyết lỗi này, chúng ta cần đi từng bước một. Đây là những điểm bạn nên kiểm tra:

1. Kiểm tra sự khác biệt DOM (DOM Mismatches)

Đây là nguyên nhân phổ biến nhất. Client-side và Server-side render ra HTML khác nhau. Hãy tập trung vào:

  • Thuộc tính HTML (Attributes): Một số thuộc tính như class, id, style, hoặc các thuộc tính tùy chỉnh có thể khác nhau.
  • Nội dung (Content): Text content hoặc số lượng/loại các phần tử con bên trong một tag có thể không khớp.

Cách debug:

  • Mở console của trình duyệt. Thông báo lỗi thường cung cấp manh mối về vị trí cụ thể của sự khác biệt.
  • Sử dụng tính năng "Inspect Element" và so sánh DOM được tạo ra ở lần tải trang đầu tiên (server-rendered) với DOM sau khi JavaScript đã chạy (client-rendered).
  • Trong React/Next.js, có thể dùng <Code class="language-jsx">if (typeof window !== 'undefined') { /* render client-only content */ }</Code> hoặc một component chuyên dụng cho client-only rendering để tạm thời loại trừ các phần có thể gây lỗi.

2. Conditional Rendering (Render khác nhau giữa server/client)

Bạn có đang render một UI khác nhau dựa trên môi trường (server hay client) mà không được xử lý cẩn thận không? Ví dụ, hiển thị một component chỉ khi window object tồn tại:

function MyComponent() {  const [isClient, setIsClient] = React.useState(false);  React.useEffect(() => {    setIsClient(true);  }, []);  if (!isClient) {    return null; // Hoặc một placeholder HTML khớp với server  }  return <p>This content only renders on the client.</p>;}

Nếu server render null và client render <p>...</p> thì sẽ có lỗi. Hãy đảm bảo server và client render ra một cấu trúc DOM tương đồng ở lần tải đầu tiên.

3. Ảnh hưởng từ Browser Extensions (Tiện ích mở rộng trình duyệt)

Nghe có vẻ lạ, nhưng một số tiện ích mở rộng có thể tiêm code hoặc chỉnh sửa DOM của trang web ngay cả trước khi JavaScript của bạn kịp chạy. Hãy thử mở trang ở chế độ ẩn danh (Incognito mode) hoặc tắt tất cả các extension để kiểm tra.

4. Cấu trúc HTML không hợp lệ

Trình duyệt đôi khi "tự sửa" HTML không hợp lệ (ví dụ: đặt <div> vào trong <p>). Nếu server trả về HTML không hợp lệ và trình duyệt tự sửa nó trước khi client-side JS chạy, thì client JS sẽ thấy một DOM khác với DOM mà server "nghĩ" nó đã gửi. Ví dụ:

<p><div>This is invalid HTML</div></p>

Trình duyệt có thể tự động tách <div> ra khỏi <p>, dẫn đến sự không khớp.

5. Third-party scripts

Các script của bên thứ ba (quảng cáo, phân tích, chat widget...) đôi khi có thể thao túng DOM trước khi quá trình hydration hoàn tất. Tạm thời vô hiệu hóa chúng để xem lỗi có biến mất không.

6. Sử dụng dangerouslySetInnerHTML

Nếu bạn dùng dangerouslySetInnerHTML, hãy chắc chắn rằng nội dung HTML bạn inject vào là nhất quán giữa server và client.

7. Manipulating DOM với useEffect/useLayoutEffect

Nếu bạn đang trực tiếp thay đổi DOM bằng document.querySelector hoặc các API tương tự trong useEffect hoặc useLayoutEffect, hãy nhớ rằng những thay đổi này chỉ diễn ra ở phía client SAU khi hydration đã xảy ra. Nếu những thay đổi đó khiến DOM khác biệt so với server-rendered HTML, lỗi có thể xuất hiện.

8. Kiểm tra typeof window hoặc process.browser

Đảm bảo logic kiểm tra môi trường của bạn là chính xác. typeof window !== 'undefined' là cách phổ biến để kiểm tra client-side. Nếu bạn dựa vào các biến môi trường như process.env.NODE_ENV để render khác nhau, hãy chắc chắn chúng được thiết lập đúng cho cả server và client build.

Lời kết

Lỗi "Hydration failed" có thể gây đau đầu, nhưng nó thường chỉ ra một sự không nhất quán cơ bản trong cách ứng dụng của bạn render UI giữa server và client. Bằng cách systematicly kiểm tra các điểm trên, bạn sẽ sớm tìm ra thủ phạm. Hãy nhớ, sự nhất quán là chìa khóa! Chúc bạn debug thành công và có những trải nghiệm phát triển web mượt mà!