تخطَّ إلى المحتوى

٩ يوليو ٢٠٢٦ · آخر تحديث: ١٩ أكتوبر ٢٠٢٦

حل خطأ Hydration failed في React و Next.js بالعربي

شارك المقال:

من أكثر الأخطاء إرباكًا للقادمين إلى 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() على خادم بتوقيت UTCtimeZone صريح، أو اعرضها بعد التركيب
فرع خادم أو متصفح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 اللانهائية.

📚 مصادر رسمية للتعمّق: التوثيق الرسمي لـ React

الأسئلة الشائعة

ما معنى الـ Hydration في React أصلًا؟

الخادم يرسل HTML جاهزًا ليظهر فورًا، ثم يأتي جافاسكريبت React في المتصفح ويربط هذا الـ HTML بالأحداث والحالة. هذه العملية اسمها Hydration (الترطيب). الخطأ يحدث حين يكتشف React أن ما رسمه في المتصفح لا يطابق ما أرسله الخادم.

ما الفرق بين رسالة React 18 ورسالة React 19؟

React 18 كان يكتب Hydration failed because the initial UI does not match what was rendered on the server وText content does not match server-rendered HTML. أما React 19 فيكتب Hydration failed because the server rendered text (أو HTML) didn't match the client، ويضيف قائمة بالأسباب المحتملة وفرقًا يوضح أين اختلفت الشجرة. السبب والحل واحد.

هل suppressHydrationWarning حل آمن؟

مقبول لحالة واحدة: عنصر نصي صغير تعرف أنه سيختلف حتمًا بين الخادم والمتصفح (كالوقت الحالي). وهو يعمل على مستوى واحد فقط: محتوى العنصر نفسه، لا أبناءه. لا تستخدمه لإسكات خطأ لا تفهم سببه.

لماذا يختفي الخطأ عند إغلاق إضافات المتصفح؟

بعض الإضافات (المترجمات، ومديرو كلمات المرور، وأدوات القواعد) تعدّل HTML الصفحة قبل أن يكمل React الـ hydration، فيرى فرقًا لم يكتبه كودك. رسالة React 19 نفسها تذكر هذا السبب. جرّب دائمًا في نافذة تصفح خاص قبل أن تطارد الخطأ في كودك.

اقرأ أيضًا

تصفّح كل المقالات في المدوّنة، أو ابدأ التعلّم من المسارات و خرائط الطريق.