درک Hydration در نکست جی‌اس

اگه با Next.js کار کرده باشی، حتماً یه بار این ارور رو دیدی: Hydration failed because the server rendered HTML didn't match the client. اولش آدم فقط یه چیزی می‌فهمه: «یه چیزی خراب شده!» ولی دقیقاً چی؟ و اصلاً این Hydration که همه ازش حرف می‌زنن چیه؟ تو این مقاله قدم‌به‌قدم می‌ریم سراغش؛ از اینکه Hydration چیه و چطوری کار می‌کنه، تا اینکه چرا ارورش میاد و چطوری درستش کنیم.

Hydration یعنی چی؟

کلمه‌ی hydration تو فارسی یعنی «آب‌رسانی» یا «آبدار کردن». تو دنیای Next.js یه مثال ساده بزنم: فرض کن یه خونه‌ی پیش‌ساخته داری که از کارخونه (سرور) کامل میاد؛ دیوار داره، پنجره داره، کاناپه داره، همه‌چی سر جاشه، ولی برق وصل نیست. می‌تونی ببینیش، ولی نمی‌تونی کلید رو بزنی و لامپ روشن بشه. Hydration همون لحظه‌ایه که برق‌کش (جاوااسکریپت) میاد و سیم‌ها رو وصل می‌کنه تا کلیدها کار کنن.

به زبون فنی‌تر: سرور کامپوننت‌های React رو به HTML تبدیل می‌کنه و می‌فرسته برای مرورگر. کاربر سریع یه صفحه‌ی کامل می‌بینه. بعد جاوااسکریپت لود می‌شه و React میاد روی همون HTML «سوار» می‌شه، event ها (مثل onClick) رو بهش وصل می‌کنه و state رو راه می‌ندازه. به این پروسه می‌گن Hydration.

فرقش با CSR چیه؟

تو یه اپ React معمولی که فقط سمت کلاینت رندر می‌شه (CSR)، مرورگر اول یه HTML تقریباً خالی می‌گیره، بعد باید کلی جاوااسکریپت دانلود و اجرا کنه تا بالاخره چیزی نشون بده. تو این مدت کاربر یه صفحه‌ی سفید می‌بینه. تو Next.js محتوا از همون اول تو HTML هست، پس کاربر زودتر چیزی می‌بینه. شکل پایین فرقشون رو نشون می‌ده:

CSR در مقابل SSR + Hydration CSR HTML خالی دانلود و اجرای JS (صفحه سفید) رندر محتوا + تعاملی ✅ SSR HTML کامل محتوا دیده می‌شه 👀 ولی هنوز کلیک کار نمی‌کنه Hydration تعاملی ✅ زمان تو SSR کاربر خیلی زودتر یه چیزی می‌بینه؛ برق (JS) بعداً وصل می‌شه
شکل ۱: کاربر تو SSR زودتر محتوا رو می‌بینه، ولی تعاملی شدنش بعد از Hydration اتفاق می‌افته.

قدم‌به‌قدم: تو پشت صحنه چی می‌گذره؟

پروسه‌ی کامل رو می‌شه تو پنج مرحله خلاصه کرد:

  1. رندر سمت سرور: Next.js کامپوننت‌های React رو روی سرور اجرا می‌کنه و یه HTML کامل می‌سازه.
  2. ارسال HTML: این HTML به‌عنوان پاسخ اولیه میره برای مرورگر و کاربر سریع صفحه رو می‌بینه.
  3. لود جاوااسکریپت: مرورگر باندل‌های JS رو دانلود و اجرا می‌کنه.
  4. مقایسه (Reconciliation): React سمت کلاینت کامپوننت‌ها رو دوباره رندر می‌کنه و نتیجه رو با HTML‌ای که سرور فرستاده مقایسه می‌کنه.
  5. وصل شدن event ها: اگه همه‌چی یکی بود، React به جای ساختن DOM از اول، همون DOM موجود رو برمی‌داره و event handler ها رو بهش وصل می‌کنه.
۱ سرور HTML می‌سازه ۲ HTML میره تو مرورگر ۳ JS لود و اجرا می‌شه ۴ React مقایسه می‌کنه ۵ event ها وصل می‌شن ✅ مراحل از چپ به راست خونده می‌شن: ۱ تا ۵
شکل ۲: مسیر یه صفحه از سرور تا لحظه‌ای که کاملاً تعاملی می‌شه.

یه مثال ساده

این یه کامپوننت شمارنده‌ست. سرور HTML‌ش رو با عدد 0 می‌سازه و می‌فرسته. تا وقتی Hydration تموم نشده، دکمه رو می‌بینی ولی کلیک روش هیچ کاری نمی‌کنه. بعد از Hydration، onClick وصل می‌شه و همه‌چی کار می‌کنه.

jsx
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      کلیک: {count}
    </button>
  );
}

یه نکته‌ی مهم: تو App Router جدید Next.js، فقط Client Component ها (اونایی که 'use client' دارن) Hydrate می‌شن. Server Component ها فقط HTML تولید می‌کنن و جاوااسکریپتی برای Hydration تو مرورگر نمی‌فرستن. یعنی هرچی بیشتر از Server Component استفاده کنی، کار Hydration کمتر می‌شه.

کدوم کامپوننت‌ها Hydrate می‌شن؟ Page (Server) Header (Server) Article (Server) LikeButton (Client) Icon (زیرمجموعه Client) فقط HTML، بدون Hydration HTML + Hydration (JS می‌ره مرورگر)
شکل ۳: فقط شاخه‌های بنفش (Client Component ها) تو مرورگر Hydrate می‌شن.

چرا Hydration به دردمون می‌خوره؟

  • سرعت بالاتر برای دیده شدن محتوا: کاربر لازم نیست منتظر دانلود کل JS بمونه تا چیزی ببینه.
  • سئو بهتر: ربات‌های موتور جستجو یه HTML کامل و آماده می‌گیرن.
  • تجربه‌ی کاربری بهتر: محتوا زود میاد و بعد کم‌کم تعاملی می‌شه.
  • Graceful degradation: اگه JS به هر دلیلی لود نشه، حداقل خود محتوا برای کاربر قابل خوندنه.
  • بهینه‌سازی‌های بعدی: بعد از Hydration، چیزایی مثل code splitting و آپدیت‌های داینامیک هم کار می‌کنن.

Hydration Error یعنی چی؟

حالا به قسمت جذاب ماجرا می‌رسیم: ارور! همونطور که گفتیم، React موقع Hydration دوباره کامپوننت‌ها رو تو مرورگر رندر می‌کنه و نتیجه رو با HTML‌ای که سرور فرستاده مقایسه می‌کنه. اگه این دو تا یکی نباشن، Hydration error می‌گیری. مثل اینه که برق‌کش بیاد و ببینه نقشه‌ی خونه با خود خونه فرق داره؛ سیم رو به کجا وصل کنه؟

وقتی سرور و کلاینت با هم یکی نیستن HTML سرور <p>Random: 0.42</p> رندر کلاینت <p>Random: 0.87</p> ≠ ⚠ Hydration failed: text content does not match
شکل ۴: سرور یه عدد تصادفی ساخته، کلاینت یه عدد تصادفی دیگه. نتیجه: ارور.

رایج‌ترین دلایل Hydration Error

تقریباً همه‌ی خطاها یه ریشه‌ی مشترک دارن: کدی که تو سرور و مرورگر نتیجه‌ی متفاوتی می‌ده. این‌ها پرتکرارترین‌هان:

  • مقادیر تصادفی یا وابسته به زمان: مثل Math.random() یا new Date(). سرور یه لحظه اجراش می‌کنه و مرورگر یه لحظه‌ی دیگه (حتی ممکنه timezone یا زبان‌شون هم فرق کنه).
  • استفاده از API های مرورگر موقع رندر: مثل window، localStorage یا navigator. سرور اینا رو نداره، پس یا ارور می‌ده یا یه مقدار پیش‌فرض می‌سازه که با کلاینت فرق داره.
  • HTML نامعتبر: مثلاً گذاشتن <div> داخل <p> یا <p> داخل <p>. مرورگر ساختار رو خودش اصلاح می‌کنه و دیگه با چیزی که React انتظار داره یکی نیست.
  • تغییر DOM توسط چیزهای بیرونی: افزونه‌های مرورگر (مثل مترجم یا ad-blocker) یا کتابخونه‌های شخص ثالث که مستقیم DOM رو دستکاری می‌کنن.
  • رندر شرطی که سرور و کلاینت جدا تصمیم می‌گیرن: مثلاً typeof window !== 'undefined' تو خود JSX.

یه مثال واقعی از ارور

jsx
// ❌ این کد Hydration error می‌ده
export default function Home() {
  const randomNumber = Math.random();

  return (
    <div>
      <p>Random Number: {randomNumber}</p>
    </div>
  );
}

سرور یه عدد می‌سازه و تو HTML می‌ذاره. وقتی همین کد تو مرورگر اجرا می‌شه، عدد دیگه‌ای تولید می‌شه و React می‌بینه که با HTML اولیه نمی‌خونه.

jsx
// ❌ مثال دیگه: ساعت
export default function Clock() {
  return <p>الان ساعت: {new Date().toLocaleTimeString()}</p>;
}

// ❌ مثال دیگه: localStorage موقع رندر
export default function Theme() {
  const theme = localStorage.getItem('theme') || 'light';
  return <p>تم: {theme}</p>;
}

// ❌ مثال دیگه: HTML نامعتبر
export default function Bad() {
  return (
    <p>
      متن
      <div>این div داخل p نباید باشه</div>
    </p>
  );
}

راه‌حل‌ها: چطوری درستش کنیم؟

۱. استفاده از useEffect برای چیزهای مخصوص کلاینت

معروف‌ترین راه همینه. ایده اینه که رندر اول (هم تو سرور، هم تو اولین رندر کلاینت) یه مقدار ثابت و یکسان داشته باشه، و بعد از اینکه Hydration تموم شد، useEffect مقدار واقعی رو بذاره. چون useEffect فقط تو مرورگر اجرا می‌شه، هیچ تفاوتی تو رندر اول پیش نمیاد.

jsx
'use client';

import { useEffect, useState } from 'react';

export default function RandomComponent() {
  const [randomNumber, setRandomNumber] = useState(null);

  useEffect(() => {
    setRandomNumber(Math.random());
  }, []);

  return (
    <div>
      <p>Random Number: {randomNumber ?? '...'}</p>
    </div>
  );
}
رندر دو مرحله‌ای با useEffect رندر سرور Random: ... اولین رندر کلاینت Random: ... یکی هستن ✅ بعد از useEffect Random: 0.87 رندر اول عمداً بی‌طرفه؛ مقدار واقعی فقط بعد از Hydration میاد (تو مرورگر، setState باعث یه رندر دوباره می‌شه)
شکل ۵: رندر اول با سرور یکیه، پس Hydration بدون مشکل تموم می‌شه؛ بعدش مقدار واقعی میاد.

همین الگو رو برای localStorage هم می‌تونی استفاده کنی:

jsx
'use client';

import { useEffect, useState } from 'react';

export default function Theme() {
  const [theme, setTheme] = useState('light'); // مقدار پیش‌فرض ثابت

  useEffect(() => {
    const saved = localStorage.getItem('theme');
    if (saved) setTheme(saved);
  }, []);

  return <p>تم: {theme}</p>;
}

۲. یه Hook کوچیک برای «آیا کلاینتم؟»

اگه این الگو رو زیاد لازم داری، یه بار می‌نویسی و همه‌جا استفاده می‌کنی:

jsx
'use client';

import { useEffect, useState } from 'react';

export function useIsClient() {
  const [isClient, setIsClient] = useState(false);

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

  return isClient;
}

// استفاده:
export default function App() {
  const isClient = useIsClient();

  return <div>{isClient ? 'سمت کلاینت' : 'سمت سرور'}</div>;
}

راه مدرن‌تر و تمیزتر هم اینه که از useSyncExternalStore استفاده کنی؛ React خودش می‌دونه موقع Hydration از مقدار سرور استفاده کنه:

jsx
'use client';

import { useSyncExternalStore } from 'react';

const subscribe = () => () => {};

export function useIsClient() {
  return useSyncExternalStore(
    subscribe,
    () => true,   // مقدار تو مرورگر
    () => false   // مقدار تو سرور و موقع Hydration
  );
}

۳. suppressHydrationWarning

بعضی وقتا تفاوت سرور و کلاینت عمدیه و بی‌ضرره؛ مثلاً نمایش تاریخ یا ساعت. تو این مواقع می‌تونی به React بگی «به هشدار این المنت اهمیت نده»:

jsx
<time dateTime="2026-10-30" suppressHydrationWarning>
  {new Date().toLocaleDateString()}
</time>

حواست باشه این فقط یه سطح کار می‌کنه (یعنی فقط روی همون المنت و متن مستقیم داخلش) و راه‌حل همه‌چی نیست. اگه ازش زیاد استفاده کنی، فقط داری ارور رو زیر فرش می‌کنی و ممکنه باگ‌های واقعی رو نبینی.

۴. غیرفعال کردن SSR برای یه کامپوننت

اگه یه کامپوننت اصلاً به مرورگر وابسته‌ست (مثلاً یه نمودار که با window کار می‌کنه)، می‌تونی بگی روی سرور رندر نشه:

jsx
'use client';

import dynamic from 'next/dynamic';

const Chart = dynamic(() => import('./Chart'), {
  ssr: false,
  loading: () => <p>در حال بارگذاری نمودار...</p>,
});

export default function Dashboard() {
  return <Chart />;
}

توجه کن که تو App Router، گزینه‌ی ssr: false فقط داخل یه Client Component کار می‌کنه (واسه همین بالاش 'use client' گذاشتیم).

۵. HTML معتبر بنویس

خیلی از ارورها فقط به خاطر ساختار اشتباه HTML هستن. چند قانون ساده:

  • تو <p> هیچ‌وقت <div> یا <p> دیگه نذار.
  • <a> تو <a> نذار (مثلاً Link داخل Link).
  • تو <table> حتماً از <tbody> و <tr> درست استفاده کن.
jsx
// ✅ درست
export default function Good() {
  return (
    <div>
      <p>متن</p>
      <div>این div بیرون از p هست</div>
    </div>
  );
}

۶. key های پایدار و useId

وقتی لیست رندر می‌کنی، به‌جای Math.random() یا ایندکس ناپایدار از یه شناسه‌ی ثابت (مثل id) به‌عنوان key استفاده کن. و اگه به یه id یکتا برای المنت‌ها (مثلاً htmlFor تو فرم) نیاز داشتی، از useId استفاده کن؛ این هوک تضمین می‌کنه مقدار سرور و کلاینت یکی باشه.

jsx
'use client';

import { useId } from 'react';

export default function EmailField() {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>ایمیل</label>
      <input id={id} type="email" />
    </div>
  );
}

۷. دیتا رو تا جای ممکن ثابت نگه دار

  • اگه دیتا قبل از رندر آماده‌ست، از سرور بگیر و به‌عنوان prop بده تا هر دو طرف یه ورودی یکسان ببینن.
  • اگه دیتا فقط تو مرورگر در دسترسه، با useEffect بگیرش و تا اون موقع یه حالت loading نشون بده.
  • تا می‌تونی از Static Generation استفاده کن؛ چون HTML ثابته، احتمال اختلاف خیلی کمتر می‌شه.
کدوم راه‌حل رو انتخاب کنم؟ مشکل چیه؟ سرور و کلاینت خروجی متفاوتی می‌دن مقدار تصادفی / زمان / localStorage / window کامپوننت کاملاً وابسته به مرورگر فقط یه متن ساده‌ی بی‌خطر (مثل تاریخ) فرق داره useEffect + useState یا useSyncExternalStore dynamic + ssr: false رندر فقط تو مرورگر suppressHydrationWarning با احتیاط!
شکل ۶: بسته به نوع مشکل، یکی از این سه راه معمولاً جواب می‌ده.

چطوری باگ رو پیدا کنم؟

  • پیام ارور رو دقیق بخون: نسخه‌های جدید React و Next.js معمولاً دقیقاً نشون می‌دن کدوم متن یا تگ فرق داره (یه diff کوچیک بین سرور و کلاینت).
  • View Source رو با Inspect مقایسه کن: View Source همون HTML سروره و Inspect DOM فعلی مرورگره. تفاوتشون سرنخ خوبیه.
  • از React DevTools استفاده کن تا ساختار کامپوننت‌ها و state رو بررسی کنی.
  • تو یه پنجره‌ی Incognito تست کن: اگه ارور رفت، مقصر احتمالاً یکی از افزونه‌های مرورگره، نه کد تو.
  • کامپوننت‌ها رو یکی‌یکی کامنت کن تا مقصر اصلی مشخص بشه.

چند نکته‌ی مدرن‌تر

از React 18 به بعد، Hydration دیگه همیشه «همه‌یا‌هیچ» نیست. با Suspense و streaming، React می‌تونه بخش‌های مختلف صفحه رو جدا از هم Hydrate کنه (Selective Hydration) و اگه کاربر روی یه بخش کلیک کرد که هنوز Hydrate نشده، اولویت رو بهش بده. نتیجه‌ش اینه که صفحه‌های سنگین سریع‌تر حس تعاملی بودن می‌گیرن.

jsx
import { Suspense } from 'react';
import Comments from './Comments';

export default function Page() {
  return (
    <main>
      <h1>مقاله</h1>

      {/* این بخش جدا از بقیه‌ی صفحه استریم و Hydrate می‌شه */}
      <Suspense fallback={<p>در حال بارگذاری کامنت‌ها...</p>}>
        <Comments />
      </Suspense>
    </main>
  );
}

جمع‌بندی

Hydration یعنی تبدیل HTML ثابتی که سرور فرستاده به یه اپ React زنده و تعاملی. تا وقتی سرور و کلاینت رندر اول یکسانی تولید کنن، همه‌چی خوب پیش می‌ره. بیشتر ارورها از اینجا میان که یه چیز متغیر (زمان، عدد تصادفی، window، localStorage) مستقیم تو رندر استفاده شده یا HTML نامعتبره. راه‌حل‌ها هم ساده‌ن:

  • چیزهای مخصوص مرورگر رو بذار تو useEffect.
  • برای کامپوننت‌های کاملاً کلاینتی از dynamic با ssr: false استفاده کن.
  • برای تفاوت‌های بی‌خطر، suppressHydrationWarning (با احتیاط).
  • HTML معتبر بنویس و از useId و key های پایدار استفاده کن.
  • تا جای ممکن از Server Component ها استفاده کن تا کمتر چیزی Hydrate بشه.

منابع