•12 min read

Lỗi Hydration trong Next.js: Cách sửa "Text Content Mismatch" & Error 418 (2026)

Lỗi Hydration trong Next.js: Cách sửa "Text Content Mismatch" & Error 418 (2026)

Lỗi Hydration trong Next.js: Nguyên Nhân và 4 Cách Khắc Phục Triệt Để

Nếu bạn từng phát triển ứng dụng với Next.js (App Router hay Pages Router) hoặc bất kỳ framework React SSR nào, chắc chắn bạn đã từng bắt gặp thông báo lỗi màu đỏ quen thuộc trong DevTools Console:

Error: Hydration failed because the initial UI does not match what was rendered on the server.
Text content does not match server-rendered HTML. (Minified React error #418)

Giao diện trang web có thể bị giật (flash), các sự kiện click không hoạt động trong tích tắc, hoặc React buộc phải xóa sạch DOM do server tạo ra để render lại từ đầu trên trình duyệt.

Bài viết này sẽ mổ xẻ chính xác bản chất của quá trình Hydration, 5 "thủ phạm" phổ biến nhất gây ra lỗi này và các giải pháp thực tế kèm code mẫu chuẩn năm 2026.


Audio Briefing
0:00 / 0:00
Interactive Dev Tool
100% Client-Side & Private

Next.js Hydration Error Matcher & Fixer

Diagnose Error #418, #423, #425 & get copy-paste code diffs

Paste Next.js or React hydration error logs to instantly diagnose server-client HTML mismatches (dates, localStorage, extension injection) and generate safe fixes.

Error 418/423SSR MismatchesSuppression FixesNext.js 14/15

Bản Chất Quá Trình "Hydration" Là Gì?

Để sửa lỗi, trước hết chúng ta cần hiểu cơ chế hoạt động của Server-Side Rendering (SSR) trong React và Next.js:

  1. Giai đoạn Pre-rendering (Server): Next.js thực thi các React component trên server Node.js, sinh ra mã HTML thô và gửi về cho trình duyệt. Người dùng nhìn thấy nội dung trang ngay lập tức (First Contentful Paint rất nhanh), nhưng trang web lúc này chưa có tính tương tác (chưa gắn các event listener như onClick, onChange).
  2. Giai đoạn Hydration (Client): Trình duyệt tải gói JavaScript bundle về. React chạy trên client, duyệt qua cây HTML do server trả về và gắn các bộ lắng nghe sự kiện để trang web bắt đầu hoạt động.

Quy tắc vàng của SSR: Cây HTML do server sinh ra phải khớp chính xác 100% với kết quả render lần đầu tiên của React trên client.

Nếu có bất kỳ sự khác biệt nào dù chỉ một ký tự hoặc một thẻ HTML, React sẽ báo lỗi Hydration Mismatch, hủy bỏ cây HTML của server và vẽ lại từ đầu, làm mất đi hoàn toàn lợi thế hiệu năng của SSR.

Giai đoạnMôi trườngĐầu raNhiệm vụ chính
Pre-rendering
Node.js Server
HTML & CSS thô
Hiển thị giao diện tức thì
Hydration
Trình duyệt (Browser)
DOM gắn Event Listener
Trang web tương tác được

Advertisement

Thủ Phạm 1: Sử Dụng Đối Tượng Toàn Cục Trình Duyệt (window & localStorage)

Server Node.js hoàn toàn không có đối tượng window, document hay localStorage. Nếu bạn truy cập trực tiếp các API này trong hàm render của component, mã nguồn sẽ gây lỗi hoặc tạo ra sự lệch pha giữa server và client.

// ❌ LỖI: Server không có đối tượng window!
function ResponsiveHeader() {
  const isMobile = window.innerWidth < 768; // Crash trên server!
  return <header>{isMobile ? 'Mobile Nav' : 'Desktop Nav'}</header>;
}

Ngay cả khi bạn kiểm tra typeof window !== 'undefined', server sẽ render nhánh false, trong khi client chạy trên trình duyệt lại render nhánh true ngay lập tức, dẫn tới lỗi lệch HTML.

tsx
1-// ❌ Hydration Error: Server render "Desktop", Client render "Mobile"
1+// ✅ An toàn: Khởi tạo mặc định trên server, cập nhật sau khi mount
2+import { useState, useEffect } from 'react';
3+
24 function ResponsiveLayout() {
3- const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
5+ const [isMobile, setIsMobile] = useState(false);
6+
7+ useEffect(() => {
8+ setIsMobile(window.innerWidth < 768);
9+ }, []);
10+
411 return <div>{isMobile ? 'Mobile' : 'Desktop'}</div>;
512 }
Mẹo thiết kế

Đối với các API trình duyệt (window, localStorage, navigator), hãy luôn đưa việc đọc dữ liệu vào hook useEffect. Vì useEffect chỉ chạy trên trình duyệt sau khi component đã mount thành công, HTML ban đầu giữa server và client luôn đồng nhất.


Thủ Phạm 2: Dữ Liệu Động (new Date(), Math.random(), Múi Giờ)

Khi bạn hiển thị thời gian hiện tại hoặc tạo số ngẫu nhiên trực tiếp trong JSX, server render ở thời điểm T_1, còn trình duyệt thực thi ở thời điểm T_2 (sau đó vài trăm mili-giây).

// ❌ Server: "10:00:00.120", Client: "10:00:00.450" -> Lệch chuỗi ký tự!
function Clock() {
  return <div>Bây giờ là: {new Date().toLocaleTimeString()}</div>;
}

Cách 1: Trì hoãn hiển thị đến khi Client mount (Khuyên dùng)

Tạo trạng thái Mounted

Tạo một state boolean mounted với giá trị mặc định là false.

Kích hoạt trong useEffect

Đổi giá trị mounted thành true bên trong useEffect.

Render có điều kiện

Hiển thị placeholder hoặc skeleton an toàn trên server, và hiển thị ngày giờ thực tế trên client.

import { useState, useEffect } from 'react';

export default function CurrentTime() {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return <div>Đang tải thời gian...</div>;

  return <div>Bây giờ là: {new Date().toLocaleTimeString()}</div>;
}

Cách 2: Sử dụng thuộc tính suppressHydrationWarning

Nếu sự khác biệt là không thể tránh khỏi và không ảnh hưởng đến logic nghiệp vụ (ví dụ: hiển thị thời gian theo múi giờ người dùng), bạn có thể thêm thuộc tính suppressHydrationWarning:

<time dateTime={dateString} suppressHydrationWarning>
  {new Date(dateString).toLocaleDateString()}
</time>
Lưu ý quan trọng

suppressHydrationWarning chỉ có tác dụng ở độ sâu 1 cấp (element trực tiếp). Không nên lạm dụng thuộc tính này để che giấu các lỗi cấu trúc HTML lớn.


Thủ Phạm 3: Lồng Thẻ HTML Sai Quy Chuẩn (Invalid Nesting)

Đây là nguyên nhân phổ biến nhất nhưng lại khó phát hiện nhất. Nó không xuất phát từ logic JavaScript mà đến từ trình phân tích cú pháp (HTML parser) của trình duyệt.

Theo đặc tả HTML5 chuẩn:

  • Thẻ <p> không được phép chứa thẻ khối như <div>, <p>, <section>, <ul>, <table>.
  • Thẻ <a> không được phép lồng trong một thẻ <a> khác.
  • Thẻ <tr> phải nằm bên trong <tbody> hoặc <thead>, không được nằm trực tiếp dưới <table>.

Khi trình duyệt thấy bạn đặt <div> bên trong thẻ <p>, trình duyệt sẽ tự động đóng thẻ <p> lại trước khi mở thẻ <div>. Cây DOM thực tế của trình duyệt bị biến đổi, không còn khớp với cây Virtual DOM mà React mong đợi từ server.

html
1-// ❌ HTML không hợp lệ: Trình duyệt tự động đóng thẻ <p> trước <div>
1+// ✅ Hợp lệ: Sử dụng các thẻ khối tương thích
2-<p>
2+<div className="space-y-2">
3- Thông báo quan trọng:
3+ <p>Thông báo quan trọng:</p>
44 <div>Nội dung chi tiết bên trong</div>
5-</p>
5+</div>

Advertisement

Thủ Phạm 4: Suspense Boundaries & Streaming SSR trong React 18 / Next.js 14+

Next.js App Router sử dụng cơ chế Streaming SSR thông qua <Suspense>. Khi dữ liệu được stream từng phần về trình duyệt, nếu component con đọc dữ liệu từ client storage khi chưa mount, quá trình giải quyết fallback của Suspense sẽ xảy ra lỗi mismatch.

// ❌ Nguy cơ lỗi khi kết hợp Suspense và LocalStorage
export default function DashboardPage() {
  return (
    <Suspense fallback={<SkeletonLoader />}>
      <UserProfile />
    </Suspense>
  );
}

function UserProfile() {
  // ❌ KHÔNG ĐỌC localStorage trực tiếp trong thân component:
  const savedToken = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
  return <div>Token: {savedToken}</div>;
}

Cách xử lý chuẩn: Luôn duy trì một state khởi tạo an toàn và chỉ cập nhật trong useEffect.


Thủ Phạm 5: Ranh Giới Server Component & Client Component ('use client')

Trong mô hình Next.js App Router:

Server Component không thể truyền các prop không thể tuần tự hóa (non-serializable) xuống Client Component.

Nếu bạn truyền một đối tượng Date, hàm Function, hoặc BigInt qua ranh giới Client Component, dữ liệu sẽ bị lỗi tuần tự hóa hoặc gây ra hydration mismatch âm thầm:

Kiểu dữ liệuHậu quả khi truyền qua propCách khắc phục
DateMismatch dữ liệu ngày thángTruyền chuỗi .toISOString()
FunctionLỗi build Next.jsChuyển logic hàm vào trong Client Component
undefinedBị chuyển đổi thành nullĐặt giá trị mặc định rõ ràng
Class instanceMất prototype methodsChuyển thành plain JSON object {}
// ✅ Chuẩn hóa ngày tháng thành chuỗi ISO trước khi truyền xuống Client Component
// app/page.tsx (Server Component)
import ClientBadge from './ClientBadge';

export default function Page() {
  const publishedAt = new Date().toISOString(); // Plain string an toàn 100%
  return <ClientBadge dateString={publishedAt} />;
}

Quy Trình Debug Lỗi Hydration Nhanh Chóng

  1. Kiểm tra thông báo chi tiết trong DevTools Console: Next.js phiên bản mới thường in rõ đoạn mã bị lệch: Expected server HTML to contain a <div> in <p>.
  2. Tắt các tiện ích mở rộng (Browser Extensions): Nhiều tiện ích như Google Translate, trình quản lý mật khẩu (1Password, LastPass) hay chặn quảng cáo thường tự ý can thiệp và chèn DOM node vào trang web, làm sai lệch DOM so với server. Hãy thử nghiệm lại trong chế độ Ẩn danh (Incognito).
  3. Kỹ thuật Dynamic Import với ssr: false: Đối với các thư viện bên thứ ba (như biểu đồ, bản đồ Leaflet) không hỗ trợ SSR, hãy tải động trên client:
import dynamic from 'next/dynamic';

const ChartWithoutSSR = dynamic(
  () => import('@/components/InteractiveChart'),
  { ssr: false, loading: () => <div>Đang tải biểu đồ...</div> }
);

Tổng Kết

Để xây dựng một ứng dụng Next.js ổn định và tải trang mượt mà:

  1. Luôn trì hoãn việc đọc window và localStorage vào useEffect.
  2. Kiểm tra tính hợp lệ của cấu trúc thẻ HTML (không lồng thẻ khối trong <p>).
  3. Chuẩn hóa dữ liệu ngày tháng thành chuỗi ISO khi truyền giữa Server và Client Component.
  4. Tham khảo thêm hướng dẫn về Cấu trúc thư mục Next.js App Router chuẩn doanh nghiệp và React 19 Server Actions & Optimistic Updates.

Kiểm tra kiến thức

Câu Hỏi Thường Gặp (FAQ)

Lỗi hydration trong Next.js là gì?

Lỗi hydration xảy ra khi cây HTML do máy chủ Node.js sinh ra không trùng khớp với cấu trúc DOM mà React tạo ra trong lần render đầu tiên trên trình duyệt. Khi xảy ra sai lệch, React sẽ ném lỗi #418 và buộc phải render lại toàn bộ component trên client, gây giật giao diện và giảm hiệu năng tải trang.

Lỗi "Text content does not match server-rendered HTML" có nghĩa là gì?

Lỗi này xuất hiện khi nội dung văn bản hiển thị giữa server và client khác nhau. Nguyên nhân phổ biến nhất là sử dụng new Date(), Math.random() trực tiếp trong JSX, hoặc kiểm tra typeof window để render các chuỗi chữ khác nhau giữa hai môi trường.

Tại sao thư viện next-themes lại gây ra lỗi hydration?

next-themes đọc lựa chọn theme (sáng/tối) của người dùng từ localStorage trên trình duyệt, trong khi server không có quyền truy cập storage này nên chỉ render theo theme mặc định. Để khắc phục, bạn chỉ cần thêm thuộc tính suppressHydrationWarning vào thẻ <html> trong file app/layout.tsx.

Làm thế nào để sử dụng localStorage trong Next.js mà không bị lỗi hydration?

Không bao giờ đọc localStorage trong lúc khởi tạo state ban đầu. Hãy khởi tạo state với giá trị mặc định an toàn (như null), sau đó sử dụng hook useEffect để đọc dữ liệu từ localStorage và cập nhật lại state sau khi trang web đã hoàn tất hydration trên trình duyệt.

Khi nào nên sử dụng thuộc tính suppressHydrationWarning?

Chỉ nên sử dụng suppressHydrationWarning cho các phần tử văn bản đơn lẻ có sự chênh lệch thời gian không thể tránh khỏi (như hiển thị ngày giờ hiện tại, múi giờ địa phương) hoặc class theme trên thẻ <html>. Tuyệt đối không dùng nó để che giấu các lỗi lồng thẻ HTML sai cú pháp.

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement