من أكثر الأخطاء إرباكًا للقادمين إلى Next.js وReact.
هذا ما ظهر لنا حرفيًا في تطبيق Next.js 16 (React 19.2) بمكوّن يعرض Math.random():
Hydration failed because the server rendered text didn't match the client.
As a result this tree will be regenerated on the client. This can happen if
a SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each
time it's called.
وفي React 18 وما قبله تراه بصيغته القديمة التي ما زال كثيرون يبحثون بها:
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"
الصفحة تعمل غالبًا، لكن React يعيد رسم الجزء المختلف كاملًا في المتصفح، فتخسر سرعة العرض من الخادم، وقد ترى الصفحة «ترمش». لنفهم السبب ونحلّه.
ماذا يحدث وقت الـ Hydration؟
في Next.js يُرسم مكوّن العميل مرتين: مرة على الخادم لإنتاج HTML يظهر بسرعة، ومرة في المتصفح ليربطه React بالتفاعل. القاعدة: الرسمتان يجب أن تتطابقا. أي فرق في النص أو البنية، حتى رقم ثانية، يكسر الربط.
(الفرق بين مكوّنات الخادم والعميل من الأساس في درس الخادم والعميل.)
اقرأ الرسالة: ثلاثة أنواع
| الرسالة (React 19) | ماذا اختلف |
|---|---|
Hydration failed because the server rendered text didn't match the client. | نص داخل عنصر |
Hydration failed because the server rendered HTML didn't match the client. | بنية العناصر نفسها |
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. | خاصية (className أو style أو href) |
In HTML, <p> cannot be a descendant of <p>. This will cause a hydration error. | تداخل وسوم غير صالح |
ورسالة React 19 تطبع تحتها فرقًا يحدد المكوّن ومكان الاختلاف بعلامتي + (العميل)
و- (الخادم). اقرأه قبل أي تخمين: غالبًا يسمّي المكوّن المسؤول مباشرة.
الأسباب وحلولها
| السبب | مثال | الحل |
|---|---|---|
| قيمة تتغير بين البيئتين | Math.random() أو Date.now() في العرض | انقلها إلى useEffect |
| منطقة زمنية أو لغة مختلفة | toLocaleString() على خادم بتوقيت UTC | timeZone صريح، أو اعرضها بعد التركيب |
| فرع خادم أو متصفح | typeof window !== "undefined" في العرض | useEffect أو dynamic بلا SSR |
| وسوم متداخلة خطأ | <p> داخل <p>، أو <div> داخل <p> | صحّح البنية |
| إضافات المتصفح | مترجم، أو مدير كلمات مرور | جرّب بنافذة خاصة |
| بيانات تتغير بين الطلبين | سعر أو عدّاد يُقرأ مرتين | مرّر القيمة نفسها من الخادم كـ prop |
1) القيم المتغيرة: اعرضها بعد التركيب
الخادم رسم الساعة 09:41، والمتصفح وصل 09:42. تعارض حتمي:
// قيمة مختلفة في كل عرض
"use client";
function Clock() {
return <span>{new Date().toLocaleTimeString("ar-EG")}</span>;
}
// الخادم والمتصفح يرسمان «—» نفسها، ثم يحدّثها المتصفح وحده
"use client";
import { useEffect, useState } from "react";
function Clock() {
const [time, setTime] = useState(null);
useEffect(() => {
setTime(new Date().toLocaleTimeString("ar-EG"));
}, []);
return <span>{time ?? "—"}</span>;
}
useEffect لا يعمل على الخادم إطلاقًا، ويعمل في المتصفح بعد الـ hydration. فأول
رسمة متطابقة في الطرفين، والتحديث يأتي بعدها كتغيير حالة عادي. المبدأ نفسه لأي
Math.random() أو قيمة من localStorage. وإن احتجت معرّفًا فريدًا ثابتًا لعنصر
(لربط label بـ input مثلًا)، فاستخدم useId() لا Math.random().
2) المنطقة الزمنية: الخطأ الذي لا يظهر على جهازك
<span>{new Date(post.createdAt).toLocaleString("ar")}</span>
لا Date.now() هنا، والتاريخ ثابت من قاعدة البيانات، ومع ذلك يظهر الخطأ عند
المستخدمين لا عندك. السبب: الخادم (Vercel وأغلب الخوادم) يعمل بتوقيت UTC، والمتصفح
بتوقيت المستخدم، فيُنسَّق التاريخ نفسه بساعة مختلفة. وعلى جهازك أنت، الخادم والمتصفح
بالتوقيت نفسه، فلا تراه أبدًا.
// ثبّت المنطقة الزمنية فيتطابق التنسيق في الطرفين
new Date(post.createdAt).toLocaleString("ar", { timeZone: "Asia/Amman" });
أو اعرض التاريخ بصيغة لا تعتمد على المنطقة (2026-10-19)، أو نسّقه بعد التركيب.
3) تداخل الوسوم: أشهر فخ صامت
HTML لا يسمح بفقرة داخل فقرة. حين يستلم المتصفح <p>…<p>…</p></p> من الخادم «يصلّحها»
بصمت إلى فقرتين متجاورتين، فيصبح DOM المتصفح مختلفًا عن شجرة React. جرّبنا صفحة
Next.js فيها <p> داخل <p>، فكتب React:
In HTML, <p> cannot be a descendant of <p>.
This will cause a hydration error.
// مكتبات الواجهات كثيرًا ما ترسم <Typography> كـ <p>
<p>
<Typography>نص</Typography> {/* <p> داخل <p> */}
</p>
// عنصر كتلي داخل فقرة
<p>اقرأ <div>هذا</div></p>
القاعدة: لا عناصر كتلية (div وp وul وtable) داخل p، ولا a داخل a، و
الجداول بـ tbody صريح. وإن كان مكوّن من مكتبة، فغيّر الوسم الذي يرسمه (أغلبها يقبل
خاصية مثل component="span" أو as="div").
4) كود المتصفح قبل أوانه
// الخادم يرسم 0، والمتصفح يرسم العرض الحقيقي
const width = typeof window !== "undefined" ? window.innerWidth : 0;
هذا الفرع بالضبط أول سبب تذكره رسالة React 19. لا تجعل نتيجة العرض الأول تعتمد على
وجود window:
// الخيار أ: بعد التركيب
const [width, setWidth] = useState(null);
useEffect(() => setWidth(window.innerWidth), []);
وإن كان المكوّن كله بلا معنى على الخادم (خريطة، أو محرر، أو رسم بياني يقرأ window):
// الخيار ب: استيراد ديناميكي بلا عرض على الخادم، داخل مكوّن عميل
"use client";
import dynamic from "next/dynamic";
const Map = dynamic(() => import("./Map"), { ssr: false });
انتبه: توثيق Next.js 16 ينص على أن ssr: false غير مسموح في مكوّنات الخادم؛ ضعه في
ملف يبدأ بـ "use client".
5) إضافات المتصفح
المترجمات وأدوات القواعد ومديرو كلمات المرور تضيف خصائص ووسومًا للصفحة قبل أن يكمل
React. رسالة React 19 نفسها تقولها: «It can also happen if the client has a browser
extension installed which messes with the HTML». افتح الصفحة في نافذة خاصة بلا
إضافات: اختفى الخطأ؟ فكودك سليم. وإن كانت الإضافة تعدّل وسم <html> أو <body>، فـ
suppressHydrationWarning على ذلك الوسم وحده مقبول.
6) الاستثناء المشروع: suppressHydrationWarning
للنصوص التي يجب أن تختلف (طابع «آخر تحديث» بتوقيت المستخدم مثلًا):
<time suppressHydrationWarning>{new Date().toLocaleString("ar")}</time>
يعمل على مستوى واحد: محتوى هذا العنصر فقط، لا أبناءه. ضعه على العنصر النصي نفسه، لا على حاوية كاملة، ولا تستخدمه لإسكات خطأ لا تفهم سببه.
قائمة تحقّق سريعة
- اقرأ الفرق الذي يطبعه React 19 تحت الرسالة: يسمّي المكوّن.
- في العرض
DateأوrandomأوlocalStorage؟ إلىuseEffect. - تاريخ يختلف عند المستخدمين لا عندك؟ المنطقة الزمنية:
timeZoneصريح. typeof windowفي العرض؟useEffectأوdynamicبلا SSR داخل مكوّن عميل.- الرسالة تذكر وسمًا؟ افحص التداخل (
pوaوالجداول). - يختفي في نافذة خاصة؟ إضافة متصفح، لا كودك.
الخلاصة
خطأ الـ Hydration جملة واحدة دائمًا: «الخادم رسم شيئًا والمتصفح رسم شيئًا آخر». اقرأ الفرق الذي يطبعه React، وحدّد مصدر الاختلاف (قيمة متغيرة، أو منطقة زمنية، أو كود متصفح، أو بنية وسوم، أو إضافة)، وطبّق حلّه. ولا تُسكت التحذير إلا للحالة النصية المشروعة.
الأساس في مسار React بالعربي ومسار Next.js، والفرق بين المكتبتين في الفرق بين Next.js وReact، وحلقات التحديث اللانهائية في حل حلقة useEffect اللانهائية.