•8 min read

suppressHydrationWarning trong Next.js: Hướng dẫn sử dụng an toàn & gỡ lỗi đầy đủ

suppressHydrationWarning trong Next.js: Hướng dẫn sử dụng an toàn & gỡ lỗi đầy đủ

Hydration là một quá trình cơ bản trong các ứng dụng React hiện đại, đặc biệt là trong các framework như Next.js, nơi Server-Side Rendering (SSR) hoặc Static Site Generation (SSG) rất phổ biến. Đây là cơ chế mà React "gắn" vào HTML được render từ máy chủ, biến đánh dấu tĩnh thành giao diện người dùng tương tác. Khi cây component React phía client không khớp chính xác với HTML được render từ máy chủ, một "lỗi không khớp hydration" xảy ra, dẫn đến các cảnh báo và khả năng không nhất quán về giao diện người dùng.

React cung cấp suppressHydrationWarning như một lối thoát để tắt các cảnh báo này. Hướng dẫn này trình bày chi tiết các trường hợp sử dụng thích hợp, những cạm bẫy tiềm ẩn và cách gỡ lỗi các vấn đề trong môi trường Next.js sản xuất bằng React 19 và Next.js 15.

Hiểu về lỗi không khớp Hydration

Lỗi không khớp hydration xảy ra khi cây component React được render trên máy chủ khác với cây component được render trên client. React mong đợi lần render phía client ban đầu sẽ tạo ra cấu trúc và nội dung DOM giống hệt như HTML được render từ máy chủ. Nếu phát hiện sự khác biệt, React sẽ ghi lại cảnh báo ở chế độ phát triển:

Warning: Prop `className` did not match. Server: "foo" Client: "bar"
Warning: Text content did not match. Server: "Hello" Client: "World"
Warning: Expected server HTML to contain a matching <tag> in <parent>.

Những cảnh báo này rất quan trọng. Chúng cho biết ứng dụng React phía client đang cố gắng tiếp quản một cấu trúc DOM không phù hợp với mong đợi của nó. Điều này có thể dẫn đến:

  1. Giảm hiệu suất: React có thể loại bỏ HTML được render từ máy chủ cho phần không khớp và render lại hoàn toàn trên client, làm mất đi một số lợi ích của SSR.
  2. Lỗi giao diện người dùng: Các nhấp nháy ngắn của nội dung không chính xác hoặc thay đổi bố cục.
  3. Vấn đề về khả năng truy cập: Trình đọc màn hình hoặc công nghệ hỗ trợ có thể tương tác với DOM không ổn định.
  4. Hành vi không mong muốn: Trình xử lý sự kiện có thể không gắn đúng cách hoặc trạng thái có thể được khởi tạo không chính xác.
Advertisement

Vai trò của suppressHydrationWarning

Prop suppressHydrationWarning là một thuộc tính boolean có thể được thêm vào bất kỳ phần tử HTML hoặc component React nào. Khi được đặt thành true, React sẽ chặn cảnh báo hydration cho phần tử cụ thể đó và các phần tử con của nó.

<div suppressHydrationWarning={true}>
  {/* Content that might cause a hydration mismatch */}
</div>

Điều quan trọng là, suppressHydrationWarning không khắc phục lỗi không khớp cơ bản. Nó chỉ đơn thuần yêu cầu React tiếp tục hydration bất chấp sự khác biệt, mà không ghi lại cảnh báo. React vẫn sẽ cố gắng đối chiếu DOM, thường bằng cách render lại phần không khớp trên client. Do đó, nó nên được sử dụng một cách thận trọng, chỉ khi lỗi không khớp được hiểu rõ, được mong đợi và vô hại.

Các trường hợp sử dụng an toàn cho suppressHydrationWarning

Có những kịch bản cụ thể mà lỗi không khớp hydration là không thể tránh khỏi hoặc vô hại, khiến suppressHydrationWarning trở thành một giải pháp chấp nhận được.

1. Tiện ích mở rộng trình duyệt

Các tiện ích mở rộng trình duyệt có thể chèn HTML tùy ý vào DOM, thường nằm ngoài tầm kiểm soát của ứng dụng của bạn. Nội dung bên ngoài này có thể khiến React phát hiện lỗi không khớp, ngay cả khi mã ứng dụng của bạn hoàn toàn nhất quán.

Ví dụ: Một tiện ích quản lý mật khẩu có thể chèn một biểu tượng bên cạnh trường nhập liệu.

// components/LoginForm.tsx
export default function LoginForm() {
  return (
    <form>
      <label htmlFor="username">Username</label>
      <input type="text" id="username" name="username" />
      <label htmlFor="password">Password</label>
      {/* Browser extensions might inject elements here */}
      <input type="password" id="password" name="password" suppressHydrationWarning />
      <button type="submit">Login</button>
    </form>
  );
}

Trong trường hợp này, áp dụng suppressHydrationWarning cho phần tử input (hoặc phần tử cha của nó div) có thể ngăn chặn các cảnh báo do sự can thiệp bên ngoài gây ra.

2. Dấu thời gian và ngày tháng

Ngày và giờ vốn dĩ là đặc trưng của client. new Date() trên máy chủ sẽ phản ánh múi giờ và thời gian của máy chủ, trong khi new Date() trên client sẽ phản ánh múi giờ và thời gian cục bộ của người dùng. Điều này thường dẫn đến sự không khớp về nội dung văn bản.

Ví dụ: Hiển thị dấu thời gian "Cập nhật lần cuối".

// components/TimestampDisplay.tsx
'use client'; // This component needs client-side execution for accurate local time

import { useEffect, useState } from 'react';

interface TimestampDisplayProps {
  isoString: string; // ISO string from server
}

export default function TimestampDisplay({ isoString }: TimestampDisplayProps) {
  const [displayTime, setDisplayTime] = useState('');

  // Option 1: Use useEffect to ensure client-side rendering only
  // This is generally preferred for complex client-side logic
  useEffect(() => {
    const date = new Date(isoString);
    setDisplayTime(date.toLocaleString());
  }, [isoString]);

  // Option 2: Use suppressHydrationWarning for simple cases where initial mismatch is acceptable
  // The server might render a different locale/timezone, but the client will quickly correct it.
  // This is applied to the element whose content might differ.
  const initialDate = new Date(isoString);
  const serverRenderedTime = initialDate.toLocaleString(); // This will be based on server's locale/timezone

  return (
    <div>
      <p>
        Last updated (Client-side useEffect): {displayTime}
      </p>
      <p suppressHydrationWarning>
        Last updated (suppressHydrationWarning): {serverRenderedTime}
      </p>
    </div>
  );
}

// Usage in a Server Component or Page:
// import TimestampDisplay from '@/components/TimestampDisplay';
// export default function MyPage() {
//   const serverTime = new Date().toISOString();
//   return <TimestampDisplay isoString={serverTime} />;
// }

Đối với phương pháp suppressHydrationWarning, lần render ban đầu sẽ sử dụng toLocaleString() của máy chủ, có thể khác với của client. Client sau đó sẽ render lại với toLocaleString() của riêng nó, ghi đè nội dung ban đầu mà không có cảnh báo. Đối với định dạng phức tạp hơn hoặc khi sự nhấp nháy ban đầu không thể chấp nhận được, phương pháp useEffect (Tùy chọn 1) vượt trội hơn vì nó đảm bảo giá trị phía client chỉ được render sau khi hydration.

3. Nhấp nháy chủ đề (Trạng thái ban đầu phía Client)

Khi chủ đề của người dùng (ví dụ: chế độ tối/sáng) được xác định phía client (ví dụ: từ localStorage hoặc prefers-color-scheme), máy chủ không thể biết chủ đề chính xác để render ban đầu. Điều này có thể dẫn đến "nhấp nháy nội dung không được định kiểu" (FOUC) hoặc lỗi không khớp chủ đề.

Ví dụ: Một bộ chuyển đổi chủ đề đọc từ localStorage.

// components/ThemeSwitcher.tsx
'use client';

import { useState, useEffect } from 'react';

type Theme = 'light' | 'dark';

export default function ThemeSwitcher() {
  const [theme, setTheme] = useState<Theme | null>(null); // Start with null to indicate unknown theme

  useEffect(() => {
    // This runs only on the client
    const storedTheme = localStorage.getItem('theme') as Theme || 'light';
    setTheme(storedTheme);
    document.documentElement.setAttribute('data-theme', storedTheme);
  }, []);

  const toggleTheme = () => {
    setTheme(prevTheme => {
      const newTheme = prevTheme === 'light' ? 'dark' : 'light';
      localStorage.setItem('theme', newTheme);
      document.documentElement.setAttribute('data-theme', newTheme);
      return newTheme;
    });
  };

  // Render a placeholder or null until theme is known client-side
  if (theme === null) {
    return <div suppressHydrationWarning style={{ visibility: 'hidden' }}>Loading theme...</div>;
  }

  return (
    <button onClick={toggleTheme} suppressHydrationWarning>
      Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
    </button>
  );
}

Ở đây, suppressHydrationWarning được áp dụng cho nút. Máy chủ có thể render một chủ đề mặc định, nhưng client sẽ nhanh chóng đọc localStorage và cập nhật chủ đề. Cảnh báo bị chặn vì lỗi không khớp ban đầu được mong đợi và nhanh chóng được giải quyết. Một giải pháp mạnh mẽ hơn cho các chủ đề thường liên quan đến một tập lệnh chạy trước khi hydration để đặt thuộc tính data-theme trên <html> dựa trên localStorage, hoặc sử dụng các biến CSS.

4. ID không xác định

Một số thư viện hoặc logic tùy chỉnh tạo ID duy nhất phía client (ví dụ: cho các thuộc tính trợ năng như aria-labelledby). Nếu các ID này được sử dụng trong lần render ban đầu, chúng sẽ khác nhau giữa máy chủ và client.

Ví dụ: Sử dụng trình tạo ID phía client tùy chỉnh (React 18+ useId xử lý điều này tự động).

// components/UniqueIdComponent.tsx
'use client';

import { useState, useEffect } from 'react';

let globalIdCounter = 0; // Not ideal for production, but demonstrates the concept

function generateUniqueId() {
  globalIdCounter += 1;
  return `client-id-${globalIdCounter}`;
}

export default function UniqueIdComponent() {
  const [id,
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