من أكثر الأخطاء إرباكًا للقادمين إلى Next.js وReact:
Error: Hydration failed because the initial UI does not match
what was rendered on the server.
أو شقيقه:
Warning: Text content did not match. Server: "09:41" Client: "09:42"
الصفحة تعمل غالبًا، لكن الرسالة الحمراء تملأ الـ console. لنفهم السبب الجذري ونحلّه بشكل صحيح.
ماذا يحدث وقت الـ Hydration؟
في Next.js يُرسم مكوّنك مرتين: مرة على الخادم (لإنتاج HTML سريع الظهور)، ومرة في المتصفح ليتولى React توصيله بالتفاعل. القاعدة الصارمة: الرسمتان يجب أن تكونا متطابقتين حرفيًّا. أي فرق — حتى مسافة أو رقم ثانية — يفجّر الخطأ.
الأسباب الأربعة وحلولها
| السبب | مثال | الحل |
|---|---|---|
| قيمة تتغير بين البيئتين | Date.now()،\u200F Math.random() | انقلها إلى useEffect |
| وسوم متداخلة بشكل غير صحيح | p داخل p، جدول بلا tbody | صحّح البنية |
| كود «متصفح فقط» في الرندر | window،\u200F localStorage | فحص التركيب أو ssr: false |
| إضافات المتصفح تعدّل الصفحة | مترجم، مدير كلمات مرور | جرّب بنافذة خاصة |
1) القيم المتغيرة: ارسمها بعد التركيب
الخادم رسم الساعة 09:41، والمتصفح وصلها 09:42 — تعارض حتمي:
// ❌ قيمة مختلفة في كل رندر
function Clock() {
return <span>{new Date().toLocaleTimeString("ar-EG")}</span>;
}
// ✅ الخادم والمتصفح يرسمان "—" المتطابقة، ثم يحدّثها المتصفح وحده
function Clock() {
const [time, setTime] = useState<string | null>(null);
useEffect(() => {
setTime(new Date().toLocaleTimeString("ar-EG"));
}, []);
return <span>{time ?? "—"}</span>;
}
نفس المبدأ لأي Math.random() أو قيمة من localStorage.
2) تداخل الوسوم: أشهر فخ صامت
HTML لا يسمح بفقرة داخل فقرة، والمتصفح «يصلّح» البنية الخاطئة بصمت — فيصبح DOM المتصفح مختلفًا عن شجرة React:
// ❌ مكتبات الـ UI كثيرًا ما ترندر <Typography> كـ <p>
<p>
<p>نص متداخل</p>
</p>
// ❌ روابط Next القديمة حول عناصر كتلية داخل فقرة
<p>اقرأ <div>هذا</div></p>
القاعدة: افحص العنصر المذكور في رسالة الخطأ (React الحديث يسمّيه) وتأكد أن
تداخله صالح في HTML — العناصر الكتلية لا تعيش داخل p.
3) كود المتصفح قبل أوانه
// ❌ window غير موجودة على الخادم أصلًا
const width = window.innerWidth;
// ✅ خيار أ: بعد التركيب
useEffect(() => setWidth(window.innerWidth), []);
وإذا كان المكوّن كله بلا معنى على الخادم (خريطة، محرر، رسم بياني):
// ✅ خيار ب: استيراد ديناميكي بلا رندر خادمي
const Map = dynamic(() => import("./Map"), { ssr: false });
4) الاستثناء المشروع: suppressHydrationWarning
للنصوص التي يجب أن تختلف (طوابع زمنية مثلًا):
<time suppressHydrationWarning>{new Date().toLocaleString("ar-EG")}</time>
استخدمه بأضيق نطاق ممكن — على العنصر النصي نفسه، وليس على حاوية كاملة.
قائمة تحقّق سريعة
- هل في الرندر
Dateأوrandomأو قراءةlocalStorage؟ →useEffect. - هل رسالة الخطأ تذكر وسمًا؟ افحص صحّة تداخله (خاصة
pوaوالجداول). - هل تعمل الصفحة في نافذة تصفح خاص؟ → المشكلة إضافة متصفح وليست كودك.
- مكوّن متصفح بالكامل؟ →
dynamic(..., { ssr: false }).
الخلاصة
خطأ الـ Hydration ليس غموضًا — هو دائمًا جملة واحدة: «الخادم رسم شيئًا والمتصفح رسم شيئًا آخر». حدّد مصدر الاختلاف (قيمة متغيرة، بنية وسوم، كود متصفح) وطبّق الحل المناسب من فوق، ولا تلجأ لإسكات التحذير إلا للحالة النصية المشروعة.
أكمل الأساس في مسار React بالعربي ومسار Next.js،
وقارن بين المكتبتين في الفرق بين Next.js و React،
وإذا طاردك خطأ undefined فدليلك في
حل Cannot read properties of undefined.