
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.
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.
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:
suppressHydrationWarningProp 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.
suppressHydrationWarningCó 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.
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.
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.
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.
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, setId] = useState<string | null>(null);
useEffect(() => {
setId(generateUniqueId());
}, []);
return <div id={id || undefined}>Client content with ID: {id}</div>;
}
Một ứng dụng Next.js thường xuyên gặp cảnh báo hydration trong môi trường production, đặc biệt liên quan đến một phần tử `<span>` có `className` khác nhau giữa server và client. Nhóm phát triển quyết định áp dụng `suppressHydrationWarning` cho `<span>` này. Kết quả có khả năng xảy ra nhất của quyết định này là gì?
Làm sạch log AI CLI, tạo biểu thức cron, giải mã JWT và tính quyền chmod offline.

Tìm hiểu sâu về cách quản lý form trong Next.js 14 bằng Server Actions, useFormState, useFormStatus và Zod để xác thực schema, giúp bạn xây dựng các form hiện đại, nâng cao dần mà không mệt mỏi với JavaScript phía client.
Đọc tiếp
Biến các lượt chia sẻ trên mạng xã hội thành động lực thúc đẩy lưu lượng truy cập tự nhiên khổng lồ bằng cách nắm vững generateMetadata của Next.js, thẻ Open Graph, Twitter Cards và tạo hình ảnh Edge OG động.
Đọc tiếp
Hướng dẫn thực tế để chẩn đoán và gỡ lỗi ranh giới Server và Client Component trong Next.js 14+ App Router - với các thông báo lỗi cụ thể, kiểm tra RSC payload, bảo vệ server-only và xử lý lỗi React 19 Server Actions.
Đọc tiếp