React تعارض الترطيب (Hydration Mismatch): كل الأسباب والحلول (2026)
١٠ أغسطس ٢٠٢٦

يحدث عدم تطابق الـ hydration في React عندما يختلف HTML الذي تم رندرتُه (render) على الخادم عن الذي يقوم React برندرته في أول تمريرة على العميل (client pass). يقوم React بتسجيل خطأ واحد مع توضيح الفرق (diff)، ثم يتخلص من HTML الخادم بدءاً من أقرب حدود Suspense نزولاً ويعيد رندرته. قم بإصلاح التباعد بدلاً من إسكات التحذير.
ملخص
توثيقات React نفسها صريحة بشأن هذا الأمر: "يجب أن تتعامل مع حالات عدم التطابق كأخطاء برمجية (bugs) وتقوم بإصلاحها".1 تذكر توثيقات React و Next.js مجتمعة تسعة أسباب — تداخل HTML غير صالح، تفرعات typeof window في الرندرة، استخدام APIs خاصة بالمتصفح فقط أثناء الرندرة، الـ APIs المعتمدة على الوقت، إضافات المتصفح، سوء تكوين CSS-in-JS، بروكسيات الحافة (edge proxies) التي تعيد كتابة HTML، المسافات البيضاء الشاردة حول جذر React، وببساطة رندرة بيانات مختلفة على كل جانب.21 بدءاً من React 19، ستحصل على خطأ واحد في الكونسول يحتوي على فرق + Client / - Server، والذي يشير عادةً مباشرة إلى العنصر المسبب للمشكلة.3
الإصلاح لا يكون أبداً باستخدام suppressHydrationWarning. هذه الخاصية (prop) تُسكت عنصراً واحداً، بعمق مستوى واحد، و React صراحةً لن يقوم بترقيع النص غير المتطابق لهذا العنصر.1 كل ما يلي مكتوب بناءً على React@19.2.8 و next@16.3.0، وهي الإصدارات التي تحمل علامة latest في npm اعتباراً من أغسطس 2026.45
ما ستتعلمه
- ماذا تعني رسالة "Hydration failed because the server rendered HTML didn't match the client" فعلياً
- القائمة الكاملة للأسباب الموثقة، مباشرة من توثيقات React و Next.js
- كيفية العثور على المكون المسؤول باستخدام الـ diff الخاص بـ React
- لماذا لا تزال إضافات المتصفح تسبب تعطل الـ hydration في React 19، رغم التحسينات
- ما إذا كانت
suppressHydrationWarningتصلح أي شيء، ومتى يكون استخدامها هو القرار الصحيح - كيفية رندرة المحتوى الخاص بالعميل فقط بشكل صحيح، باستخدام
useEffectمقابلuseSyncExternalStore - كيفية منع التواريخ والأوقات والمواقع (locales) من التباعد بين الخادم والعميل
- لماذا يتسبب
next/dynamicمعssr: falseفي حدوث خطأ في App Router - لماذا يمكن أن تتباين قيم
useIdبين الخادم والعميل - ما إذا كان الـ CDN أو بروكسي الحافة يمكن أن يكون هو السبب
- ما هي تكلفة عدم تطابق الـ hydration في بيئة الإنتاج (production)
ماذا تعني رسالة "Hydration failed because the server rendered HTML didn't match the client"؟
هذا يعني أن React ارتبط بـ HTML تم إنشاؤه بواسطة الخادم، وقام بتصيير شجرة المكونات الخاصة بك مرة واحدة في المتصفح، وحصل على نتيجة مختلفة. الـ Hydration هي الخطوة التي يقوم فيها React "بتحويل HTML الذي تم تصييره مسبقاً من الخادم إلى تطبيق تفاعلي بالكامل عن طريق ربط معالجات الأحداث (event handlers)."2 عندما يختلف التصييران، لا يستطيع React ربط تلك المعالجات بأمان بـ DOM الموجود، لذا يقوم بالتخلص من HTML الخادم وإعادة التصيير على العميل. نطاق التأثير أكبر مما يتوقعه معظم الناس؛ حيث يتراجع React إلى أقرب حدود Suspense فوق عدم التطابق ويقوم بالتصيير من هناك على العميل؛ وفي حال عدم وجود حدود فوقه، فهذا يعني التخلص من HTML الخادم للجذر بالكامل.6 تصف ملاحظات الإصدار الخاصة بـ React نفس النتيجة من الاتجاه الآخر، مشيرة إلى ما يحدث "إذا احتاج React إلى إعادة تصيير المستند بالكامل بسبب عدم تطابق hydration غير مرتبط."3
قبل إصدار React 19، كان هذا يظهر كمجموعة من التحذيرات المنفصلة — Warning: Text content did not match. Server: "Server" Client: "Client"، يليه Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.، يليه خطأ يتم إطلاقه.3 قام React 19 بدمج كل ذلك في رسالة واحدة:
Uncaught Error: Hydration failed because the server rendered HTML didn't match the
client. As a result this tree will be regenerated on the client. This can happen if
an 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.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with
the HTML before React loaded.
https://react.dev/link/hydration-mismatch
قائمة النقاط هذه ليست نصيحة عامة — بل هي React يخبرك بأي من الفئات الخمس يجب أن تتحقق أولاً.3
ما الذي يسبب عدم تطابق hydration في React؟
ينشر Next.js قائمة مرقمة من سبعة أسباب في صفحة الخطأ الخاصة بهذه الرسالة. أما مرجع hydrateRoot الخاص بـ React فينشر قائمة أقصر من أربعة أسباب، اثنان منها يتداخلان مع Next.js واثنان منها إضافات.21 إذا جمعناهم سنحصل على تسعة. الأسباب من 1 إلى 7 أدناه هي قائمة Next.js بترتيبها الخاص، والسببان 8 و 9 هما إضافات React، والعمود الأيمن هو اختصارنا التشخيصي الخاص بدلاً من أي شيء مذكور في أي من المستندين:
| # | السبب (من التوثيق) | العلامة النموذجية (عندنا) |
|---|---|---|
| 1 | تداخل HTML غير صالح — <div> أو <ul> داخل <p>، أو <a> داخل <a>، أو <button> داخل <button> | يظهر الـ Diff عنصراً قام المتصفح بنقله بصمت |
| 2 | فرع typeof window !== 'undefined' في عملية الـ render | يظهر المحتوى فقط بعد تحميل JS |
| 3 | استخدام APIs خاصة بالمتصفح فقط مثل window، أو localStorage أو window.matchMedia أثناء الـ render | قراءة محمية تعيد قيمة على السيرفر وقيمة أخرى في المتصفح |
| 4 | APIs تعتمد على الوقت مثل constructor الـ Date() | تختلف الطوابع الزمنية (Timestamps) بأجزاء من الثانية أو حسب المنطقة الزمنية |
| 5 | إضافات المتصفح التي تعدل HTML قبل تحميل React | تتكرر المشكلة في ملفك الشخصي اليومي، وليس في ملف نظيف |
| 6 | مكتبات CSS-in-JS مهيأة بشكل خاطئ | يظهر الـ Diff قيمتين مختلفتين لـ className تم توليدهما |
| 7 | طبقة Edge/CDN تقوم بإعادة كتابة استجابة HTML | تتكرر المشكلة من خلال الـ CDN، وليس عند الاتصال بالمصدر (origin) |
| 8 | مسافات فارغة إضافية أو أسطر جديدة حول جذر React داخل غلاف HTML | يشير الـ Diff إلى عقدة نصية (text node) لم تكتبها أبداً |
| 9 | عرض بيانات مختلفة على كل جانب — مثل sort غير حتمي، أو Math.random()، أو مخزن خارجي غير مأخوذ منه لقطة (un-snapshotted) | يظهر الـ Diff محتوى حقيقياً في كلا العمودين، ولكن بترتيب خاطئ أو بقيم خاطئة |
السبب 8 يستحق تنبيهاً: فهو ينطبق على أغلفة HTML المكتوبة يدوياً، لذا يمكن حدوثه في Vite أو إعداد SSR مخصص، ولكنه بعيد المنال إلى حد كبير في App Router، حيث لا تقوم بكتابة الغلاف بنفسك. السبب 9 هو الأشمل من بين التسعة والأسهل في التغافل عنه، لأن لا شيء في الكود يبدو مرتبطاً ببيئة معينة — يذكره React ببساطة كـ "عرض بيانات مختلفة على السيرفر والعميل."1
كيف أجد المكون الذي تسبب في عدم تطابق الـ hydration؟
اقرأ الـ diff. يقوم React 19 بطباعة المسار وصولاً إلى عدم التطابق ويحدد نقطة الاختلاف بأسطر + Client و - Server. المدخل الموجود مباشرة فوق العلامة هو العنصر الذي اختلف، والأسماء الموجودة فوق ذلك هي المكونات التي قامت بعرضه:3
<App>
<span>
+ Client
- Server
لقد تطورت الأدوات هنا. وفر Next.js 16.2 مؤشر Hydration Diff في غطاء أخطاء التطوير (dev error overlay) الذي يحدد نفس الاختلاف بأسطورة + Client / - Server، لذا لم تعد مضطراً لقراءتها من مخرجات الكونسول الخام.7
ثلاث خطوات تالية تغلق الحلقة بسرعة:
-
أعد التحميل في ملف متصفح نظيف بدون أي إضافات مثبتة. إذا اختفى الخطأ، فمن المرجح أن تكون الإضافة هي السبب. لا تستخدم نافذة خاصة (private window) لهذا الاختبار: لأنها تسقط أيضاً الـ service workers، و
localStorage، والكوكيز، وتوثيق الهوية المخزن مؤقتاً، وأي من هذه قد يكون هو سبب الاختلاف الحقيقي، لذا فإن النجاح في هذا الاختبار لا يثبت الكثير. تأكد من ذلك عن طريق إعادة تفعيل الإضافات واحدة تلو الأخرى في ملفك الشخصي اليومي. -
عرض HTML الخام الخاص بالسيرفر. يقوم Next.js بإصدار المستند أساساً كسطر واحد، لذا فإن تمريره مباشرة إلى
grepيعيد لك الصفحة كاملة. قم بتقسيمها بناءً على الوسوم (tags) أولاً — لاحظ أنtrلا يمكنه فعل ذلك، لأنه يتعامل مع أحرف مفردة وسيقوم بتجاهل العملية بصمت هنا:curl -s http://localhost:3000/your-route | perl -pe 's/>/>\n/g' | grep 'suspicious-string'هذا يوضح لك بالضبط ما أصدره السيرفر، بدون وجود متصفح أو إضافة في المنتصف.
التقسيم الثنائي باستخدام التعليقات (Bisect with comments). قم بتحويل نصف الشجرة المشتبه بها إلى تعليقات، ثم أعد التحميل، وكرر العملية. تنجح هذه الطريقة عندما يكون السبب مستقراً — مثل التداخل (nesting)، أو CSS-in-JS، أو المسافات البيضاء. أما تعارضات الإضافات (Extensions) والتوقيت فقد تكون متقطعة، لذا استبعدها أولاً وإلا فإن عملية التقسيم ستطارد ضوضاءً لا فائدة منها.
إذا كنت تعمل في قاعدة بيانات Server Components وغير متأكد من الأجزاء التي يتم عمل hydrate لها، فإن دليلنا حول React Server Components والحدود بين الخادم والعميل يغطي المكونات التي يتم إرسالها إلى المتصفح في المقام الأول.
لماذا يظهر خطأ الـ hydration فقط عند تثبيت إضافة للمتصفح؟
لأن الإضافات تقوم بتغيير الـ DOM في الفترة الزمنية بين وصول الـ HTML وقيام React بعمل hydrate لها. وهناك مثال موثق جيداً في مناقشة على Next.js حيث تقوم إضافة ColorZilla بإضافة cz-shortcut-listen="true" إلى <body>. قام أحد مطوري Next.js بتحويل تقرير الخطأ إلى مناقشة بدلاً من إغلاقه، مشيراً إلى أنه "بما أن أخطاء الـ hydration هذه تأتي من React وليس من Next.js، فإن هذا لا يمكن التعامل معه كتقرير خطأ (bug report)."8 ونشر معلق آخر في نفس السلسلة فرقاً (diff) يظهر حقن id="scrnli_recorder_root" من إضافة لتسجيل الشاشة، وهناك مناقشة منفصلة في Next.js تتبع سمة data-lt-installed الخاصة بـ LanguageTool.89
هذا هو الجزء الذي تخطئ فيه معظم المقالات. لقد قام React 19 بتحسين التوافق مع الإضافات، ولكن التحسين أضيق مما يوحي به العنوان: "سيتم تخطي الوسوم غير المتوقعة في <head> و <body>، مما يتجنب أخطاء عدم التطابق."3 كلمة "وسوم" (Tags) هنا تحمل معنىً محدداً جداً. فالإضافة التي تضيف <div> مباشرة إلى <body> أصبحت مقبولة الآن؛ أما الإضافة التي تضيف سمة (attribute) إلى عنصر يمتلكه React بالفعل فهي ليست كذلك. واستمرت السلسلة في جمع التقارير حتى بعد الإصدار المستقر من React 19 وحتى نوفمبر 2025 على الأقل — بعد فترة طويلة من إطلاق التحسين.8
إذن، ماذا تفعل فعلياً؟ الإجابة التي قبلها صاحب التقرير كانت جملة واحدة — "أضف suppressHydrationWarning إلى وسم body" — والتي تبدو في App Router root layout بهذا الشكل:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body suppressHydrationWarning>{children}</body>
</html>
)
}
أكد صاحب التقرير أن هذا حل مشكلته.8 وهذا الحل مبرر هنا تحديداً لأن التغيير يحدث على عنصر تملكه، على مستوى واحد من العمق، وهو خارج نطاق تحكمك. فضل أحد المشاركين في السلسلة مساراً مختلفاً تماماً — تهيئة ColorZilla ليتعامل مع الصفحة فقط عند النقر على الإضافة، وبذلك لا تكون هناك حاجة إلى أي كبت (suppression) على الإطلاق.8
هل يقوم suppressHydrationWarning فعلياً بإصلاح عدم تطابق الـ hydration؟
لا. فهي تقوم بكتم التحذير لعنصر واحد يكون فيه "محتوى السمة أو النص مختلفاً بشكل لا يمكن تجنبه بين الخادم والعميل"، وتضع وثائق React حدين صارمين: "هذا يعمل فقط على مستوى واحد من العمق، وهو مخصص ليكون مخرجاً اضطرارياً"، و"React لن تحاول إصلاح محتوى النص غير المتطابق".1 سيظل المستخدمون يرون قيمة الخادم حتى يتم إعادة رندرة (re-render) العنصر.
قطعتان من الكود، بافتراض وجود prop من نوع isoDate في النطاق:
// Legitimate: one element, genuinely unavoidable, no children depend on it.
// dateTime carries the machine-readable value so the markup stays valid.
<time dateTime={isoDate} suppressHydrationWarning>
{new Date(isoDate).toLocaleTimeString()}
</time>
// Not legitimate: the mismatch is inside a child, so this suppresses nothing
<div suppressHydrationWarning>
<UserGreeting /> {/* still throws */}
</div>
يحتوي نفس موضوع النقاش على مثال مضاد واضح. أبلغ أحد المطورين عن نفس فشل الـ hydration دون وجود أي إضافات، ولم يفعل suppressHydrationWarning شيئاً. كان السبب هو مكتبة CSS-in-JS تقوم بتوليد أسماء classes مختلفة على الخادم والعميل؛ وشخص أحد المتعاونين المشكلة بأن "أسماء الـ classes التي يتم توليدها في الخادم والعميل متباعدة، وهذا يحدث عادةً عندما لا تكون مكتبة CSS in JS مهيأة لـ SSR"، وكان الحل الحقيقي هو إعادة هيكلة محددات الـ class المتداخلة.8 الكتم لا يمكن أن يساعد عندما يكون عدم التطابق هيكلياً.
كيف أقوم برندرة محتوى خاص بالعميل فقط بدون حدوث hydration mismatch؟
اجعل الرندرة الأولى على العميل تطابق الخادم، ثم قم بالتحديث. تسمي React هذا بـ "الرندرة على مرحلتين" (two-pass rendering)، وهو النهج الموثق رسمياً:1
'use client'
import { useState, useEffect } from 'react'
export default function PublishedAt({ isoDate, serverFormatted }) {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// First client render returns the server's string verbatim, so hydration
// matches. The Effect then re-renders in the visitor's own locale.
return (
<time dateTime={isoDate}>
{mounted
? new Date(isoDate).toLocaleString(undefined, { dateStyle: 'medium' })
: serverFormatted}
</time>
)
}
تضع React تكلفة صريحة لهذا النمط: "هذا النهج يجعل الـ hydration أبطأ لأن المكونات يجب أن يتم رندرتها مرتين". تخبرك الوثائق أيضاً بأن "تكون مدركاً لتجربة المستخدم على الاتصالات البطيئة"، لأن JavaScript قد يصل بعد فترة طويلة من الـ HTML الأولي، وتبديل واجهة المستخدم فوراً بعد الـ hydration "قد يبدو أيضاً مزعجاً للمستخدم".1 اجعل هذا الحل مخصصاً للقيم التي تخص العميل فقط حقاً، ويفضل استخدام placeholder يتم رندرته على الخادم يشغل نفس مساحة المحتوى النهائي حتى لا يتحرك أي شيء.
هذا هو الشكل الصحيح لقيمة يتم قراءتها مرة واحدة وليس لها store خلفها. لأي شيء مدعوم بـ API متغيرة في المتصفح أو store خارجي، يغطي القسم التالي أداة بدائية أفضل.
هل يجب أن أستخدم useEffect أم useSyncExternalStore للقيم الخاصة بالعميل فقط؟
استخدم useSyncExternalStore كلما كانت القيمة قادمة من API متغيرة في المتصفح أو store خارج React — مثل navigator.onLine، أو matchMedia، أو store بنمط Zustand. فهي تأخذ وسيطاً ثالثاً، getServerSnapshot، الذي "يعمل على الخادم عند توليد الـ HTML" و "يعمل على العميل أثناء الـ hydration"، وهو بالضبط الضمان الذي يتطلبه تجنب الـ hydration mismatch.10
'use client'
import { useSyncExternalStore } from 'react'
// Declared outside the component so React does not re-subscribe on every render.
function subscribe(callback) {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
}
const getSnapshot = () => navigator.onLine
const getServerSnapshot = () => true // what the server HTML will say
export function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
}
هناك قاعدتان من الوثائق تحددان ما إذا كان هذا سيعمل. إذا أغفلت getServerSnapshot فإن "عملية رندرة المكون على السيرفر ستؤدي إلى حدوث خطأ". وتأكد من أنه "يعيد نفس البيانات بالضبط في أول رندرة على العميل كما أعادها على السيرفر" — إذا قام السيرفر بتحميل محتويات المتجر مسبقاً، يجب عليك إرسال تلك اللقطة (snapshot) إلى العميل، عادةً عبر <script> يقوم بتعيين متغير عام يقرأه العميل مرة أخرى.10 هذا لا يلغي الرندرة الثانية. إذا أعادت getServerSnapshot القيمة true وكان الزائر في الواقع غير متصل بالإنترنت، فإن React يتم عمل hydrate له كـ "Online" ويصحح نفسه فوراً بعد ذلك. ما تكسبه مقارنة بـ useEffect هو أن السيرفر والعميل يتفقان بناءً على التصميم بدلاً من تذكرك لتقييد المرور الأول، بالإضافة إلى اشتراك مباشر، بدون الحاجة إلى هيكلية useState التي قد تخطئ في تنفيذها.
كيف يمكنني إصلاح عدم تطابق الـ hydration الناتج عن التواريخ والأوقات والإعدادات المحلية (locales)؟
أحد المحفزات الخمسة المذكورة لـ React يتعلق مباشرة بالوقت — "تنسيق التاريخ في الإعدادات المحلية للمستخدم والتي لا تتطابق مع السيرفر" — ومحفز ثانٍ، "المدخلات المتغيرة مثل Date.now() أو Math.random() التي تتغير في كل مرة يتم استدعاؤها،" والتي تضع الساعة كأحد مثالين لها.3 مشكلة الإعدادات المحلية هي الأكثر خداعاً، لأن toLocaleDateString() يتم حلها بناءً على الإعدادات المحلية والمنطقة الزمنية لبيئة التشغيل (runtime) — وعادةً ما يعمل حاوية السيرفر الخاصة بك بتوقيت UTC، ولا يشترط أن تتطابق بيانات الإعدادات المحلية الخاصة بها مع لابتوب الزائر.
// Diverges: server and client resolve locale and timezone independently
<span>{new Date(post.publishedAt).toLocaleDateString()}</span>
// Stable: both sides are pinned to the same locale and timezone
<span>
{new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'UTC',
}).format(new Date(post.publishedAt))}
</span>
تثبيت الإعدادات (Pinning) يزيل التباعد الافتراضي لبيئة التشغيل، ولكنه ليس ضماناً لمخرجات متطابقة تماماً على مستوى البايت، لأن Node والمتصفحات تستخدم إصدارات ICU مختلفة وتغييرات مراجعات ICU تغير بيانات الإعدادات المحلية. قام ICU 72 بتحديث عدة إعدادات محلية لإصدار مسافة ضيقة غير قابلة للكسر (U+202F) قبل AM/PM حيث كانت هناك مسافة عادية، مما أدى إلى كسر مقارنات النصوص عبر إصدارات Node — وهو بالضبط نوع الاختلاف في رمز واحد (one-codepoint) الذي يظهر كعدم تطابق في الـ hydration.11 إذا كنت بحاجة إلى ضمان تام، قم بالتنسيق إلى نص عادي على السيرفر ومرره كـ prop، بحيث تقوم بيئة تشغيل واحدة فقط بعملية التنسيق.
إذا كنت تريد حقاً التنسيق المحلي للزائر، قم برندرة النسخة المثبتة على السيرفر وأعد التنسيق في Effect، أو سلم العنصر بالكامل إلى suppressHydrationWarning كما يفعل مثال الطابع الزمني الخاص بـ React.1 هناك ملاحظة جانبية تستحق المعرفة. توضح Next.js أن iOS "يحاول اكتشاف أرقام الهواتف وعناوين البريد الإلكتروني والبيانات الأخرى في محتوى النص وتحويلها إلى روابط، مما يؤدي إلى عدم تطابق في الـ hydration،" وتقدم خيار إلغاء ذلك:2
<meta
name="format-detection"
content="telephone=no, date=no, email=no, address=no"
/>
في App Router، ستقوم عادةً بإصدار ذلك من خلال حقل formatDetection في Metadata الخاصة بـ API بدلاً من كتابة الوسم يدوياً، ولكن التأثير هو نفسه.
لماذا يتسبب next/dynamic مع ssr: false في حدوث خطأ في App Router؟
لأن الصفحة في App Router تكون Server Component بشكل افتراضي، و Next.js لا يسمح بهذا الخيار هناك. التوثيق صريح في ذلك: "خيار ssr: false غير مدعوم في Server Components. ستظهر لك رسالة خطأ إذا حاولت استخدامه في Server Components،" ونص الخطأ الذي يظهر لك هو "ssr: false غير مسموح به مع next/dynamic في Server Components. يرجى نقله إلى Client Component."12
// app/page.tsx — throws, because this file is a Server Component
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./chart'), { ssr: false })
انقل الاستيراد الديناميكي (dynamic import) إلى Client Component، ثم قم بتشغيله من الصفحة:
// app/chart-island.tsx
'use client'
import dynamic from 'next/dynamic'
// ./chart lands in the client bundle automatically, because the module that
// imports it is a Client Component. It needs no 'use client' of its own.
const Chart = dynamic(() => import('./chart'), { ssr: false })
export default function ChartIsland() {
return <Chart />
}
// app/page.tsx — corrected: the Server Component renders the island
import ChartIsland from './chart-island'
export default function Page() {
return <ChartIsland />
}
تذكر نفس الصفحة هذا القيد بشكل إيجابي أيضاً: "خيار ssr: false سيعمل فقط مع Client Components، انقله إلى Client Components لضمان عمل تقسيم الكود (code-splitting) في العميل بشكل صحيح."12 لذا فإن النمط المتبع هو إنشاء غلاف (wrapper) صغير بـ 'use client' يمتلك الاستيراد الديناميكي، ويتم تشغيله من Server Component. تخطي SSR يزيل عدم التطابق عن طريق إزالة الرندرة من السيرفر، وهو ما يكلفك فقدان HTML المولد من السيرفر لهذا الجزء من الشجرة. تعامل مع هذا الحل كملاذ أخير للأدوات (widgets) التي تعمل حصرياً على المتصفح، وليس كحل عام.
لماذا تختلف قيم useId بين السيرفر والعميل؟
الإجابة المعتادة أقل إثارة من الإجابة الأخرى: السيرفر والعميل قاما برندرة أشجار مختلفة هيكلياً. يقوم React بتوليد كل ID من "مسار الأب" للمكون المستدعي عبر الشجرة بدلاً من استخدام عداد تصاعدي — وهذا بالضبط ما يجعل الـ IDs مستقرة عبر اختلاف ترتيب الرندرة، وهو بالضبط سبب عدم استقرارها عبر اختلاف شكل الرندرة.13 وجود فرع يرندر شكلاً مختلفاً على كل جانب — مثل حالات typeof window وشكوك المتصفح-API الناتجة عن السببين 2 و 3 — يكفي لكسر هذا التطابق. توثيقات React تضع المتطلب بوضوح: يحتاج useId إلى شجرة مكونات متطابقة على السيرفر والعميل. قم بمراجعة هذه الأسباب أولاً.
السبب المثير للاهتمام يستحق الاستبعاد مبكراً على أي حال، لأن التحقق منه لا يتطلب سوى أمر واحد: وجود إصدارين مختلفين من React في نفس الشجرة. قام React بتغيير البادئة الافتراضية لـ useId مرتين: :r: في 19.0.0، و «r» في 19.1.0، و _r_ في 19.2 — التغيير الأخير تم لضمان أن تكون الـ IDs المولدة صالحة لـ view-transition-name وأسماء XML 1.0.14 إذا كانت حزمة SSR وحزمة العميل لديك تشيران إلى إصدارات فرعية مختلفة من React، فإن كل useId في الشجرة سينحرف، وستحصل على سيل من حالات عدم التطابق دون سبب واضح على مستوى التطبيق.
# Rule out a duplicated React in the install tree — the cheapest check
npm ls react react-dom
أي مخرجات تظهر إصدارين، أو زوجاً من React و React-dom غير متطابقين، هي الشيء الذي يجب إصلاحه أولاً — قم بإزالة التكرار (deduplicate) قبل البحث أكثر. هذا يفحص الشجرة المثبتة بدلاً من ما تحله حزمك فعلياً، لذا في حالة monorepo تحقق من الـ aliases وأي حزم خارجية أيضاً. في pnpm أو Yarn، الأمر المكافئ هو pnpm why React أو yarn why React. الخيار المرتبط بذلك هو خيار الجذر identifierPrefix، والذي يصفه React بأنه "بادئة نصية يستخدمها React للـ IDs المولدة بواسطة useId... يجب أن تكون نفس البادئة المستخدمة على السيرفر."1 إذا كنت تشغل عدة جذور React في صفحة واحدة وقمت بتعيين identifierPrefix على العميل، فقم بتعيين القيمة المطابقة على رندر السيرفر.
هل يمكن لـ CDN أو edge proxy أن يسبب عدم تطابق في الـ hydration؟
نعم، وهذا هو فرع قائمة الأسباب الذي يميل إلى الظهور في بيئة الإنتاج فقط. تدرج Next.js "إعدادات Edge/CDN غير الصحيحة التي تحاول تعديل استجابة html" كسبب سابع وتسمي Cloudflare Auto Minify كمثال.2 أي طبقة تعيد كتابة HTML — سواء كانت أدوات التصغير (minifiers)، أو معالجات HTML اللاحقة، أو وسوم التحليلات المحقونة — تغير البايتات التي تقوم React بعملية الـ hydration مقابلها دون تغيير الكود الخاص بك.
وضع Cloudflare هنا يتطلب الحذر، لأن الاستنتاج البديهي خاطئ. تؤرخ صفحة الإلغاءات (deprecations) الخاصة بـ Cloudflare ميزة Auto Minify في 5 أغسطس 2024 وتدرج GET و PATCH /zones/:zone_id/settings/minify كواجهات برمجة تطبيقات (APIs) مهجورة.15 كلمة "مهجورة" لا تعني أنها اختفت. أوضح أحد موظفي Cloudflare هذا الانقسام في منتدى الشركة الخاص: "تمت إزالة واجهة المستخدم لمنع المزيد من التفعيل بينما نعمل على إيقاف التشغيل الكامل للخلفية. في هذه الأثناء... لا يزال بإمكانك إدارة ذلك عبر API."16 لهذا السبب لا تزال Cloudflare تحتفظ بصفحة لاستكشاف الأخطاء وإصلاحها، تم تحديثها في أبريل 2026، والتي تبدأ جملتها الافتتاحية بـ "إذا كان موقعك لا يزال يستخدم ميزات مهجورة لـ Auto Minify، فقم بإيقاف تشغيل Auto Minify عبر API."17 أي منطقة (zone) كانت الميزة مفعلة فيها قبل اختفاء زر التبديل لا تزال قادرة على تصغير HTML الخاص بك. تحقق من الإعداد مباشرة بدلاً من الافتراض:
curl "https://api.cloudflare.com/client/v4/zones/{zone_id}/settings/minify" \
--header "Authorization: Bearer <API_TOKEN>"
إذا عادت أي من css، أو html، أو js بقيمة "on"، فاستخدم PATCH لنفس نقطة النهاية لإيقاف تشغيلها.17 التشخيص العام ينطبق على كل CDN: استخدم curl لرابط الإنتاج، و curl لمصدرك (origin) مباشرة، وقارن بين الاستجابتين. إذا اختلفتا، فإن الـ edge يعيد كتابة HTML الخاص بك.
هل تؤثر تعارضات الـ hydration في بيئة الإنتاج، أم في بيئة التطوير فقط؟
إنها تؤثر في بيئة الإنتاج. إرشادات React واضحة تماماً: "React يتعافى من بعض أخطاء الـ hydration، ولكن يجب عليك إصلاحها مثل أي أخطاء أخرى. في أفضل الحالات، ستؤدي إلى تباطؤ؛ وفي أسوأ الحالات، قد يتم ربط معالجات الأحداث (event handlers) بالعناصر الخاطئة."1 التباطؤ يشمل كل شيء بدءاً من أقرب حدود Suspense نزولاً، حيث يتم إعادة رندرة العناصر من جهة العميل (client-side)، مما يضيع الجهد الذي بذله الـ SSR وقد يؤدي إلى إزاحة في التنسيق (layout shift) بعد الرسم. تغليف الأشجار الفرعية الخطرة في <Suspense> لن يصلح التعارض، ولكنه يحدد مقدار الصفحة الذي يمكن أن يتأثر بهذا التعارض. أسوأ حالة هي خطأ في الدقة — مثل نقرة تقع على الصف الخاطئ.
تختلف بيئة التطوير عن بيئة الإنتاج في الرؤية، وليس في الخطورة. "في وضع التطوير، تحذر React من التعارضات أثناء الـ hydration. لا توجد ضمانات بأن اختلافات السمات (attributes) سيتم إصلاحها في حالة التعارضات"، وهذا هو السبب في أن التطبيق الصامت في بيئة الإنتاج ليس دليلاً على خلوه من المشاكل.1 لرؤيتها في بيئة الإنتاج، قم بتفعيل خيار الجذر onRecoverableError، والذي تصفه React بأنه "عندما تتعافى React تلقائياً من الأخطاء":1
import { hydrateRoot } from 'react-dom/client'
import App from './App'
import { yourErrorReporter } from './reporting'
hydrateRoot(document.getElementById('root'), <App />, {
onRecoverableError: (error, errorInfo) => {
yourErrorReporter({ error, componentStack: errorInfo.componentStack })
},
})
ينطبق هذا الجزء فقط إذا كنت تملك استدعاء hydrateRoot. في App Router الخاص بـ Next.js، أنت لا تملك ذلك، ولا يوفر Next.js خيار onRecoverableError كخيار إعداد عام — لذا فإن التقاط هذه الأخطاء في بيئة الإنتاج يعني استخدام SDK لمراقبة الأخطاء يقوم بتجهيز جذر React نيابة عنك. يُعد SDK الخاص بـ Sentry لـ Next.js هو الخيار المعتاد؛ وأياً كان اختيارك، تأكد من أنه يبلغ عن الأخطاء القابلة للاسترداد (recoverable errors) وليس فقط الأخطاء التي يتم إلقاؤها (thrown errors)، لأن عدم تطابق الـ hydration يندرج تحت النوع الأول.
الخلاصة
يُعد عدم تطابق الـ hydration في React خطأً في الحتمية (determinism bug)، وقائمة الأشياء التي تكسر الحتمية قصيرة بما يكفي لمعالجتها في جلسة واحدة. اقرأ الـ diff، وقم بتضييق نطاق الإضافات (extensions) عن طريق تعطيلها واحدة تلو الأخرى في ملف تعريفك اليومي، ثم تتبع الأسباب التسعة الموثقة بالترتيب. بالنسبة للقيم التي تأتي من API متغيرة في المتصفح أو مخزن خارجي، استخدم useSyncExternalStore قبل useEffect؛ واستخدم useEffect قبل ssr: false؛ ولا تستخدم suppressHydrationWarning إلا عندما يكون الاختلاف خارج نطاق تحكمك تماماً ومقتصرًا على عنصر واحد.
هناك عادتان تمنعان تكرار هذه المشكلات: تثبيت كل تنسيق تاريخ على locale ومنطقة زمنية محددة، وتشغيل npm ls React React-dom بعد أي تغيير في التبعيات (dependencies) لضمان عدم تباعد أدوات الرندرة (renderers) في السيرفر والعميل.
بعد ذلك، إذا كنت تقوم بتصحيح أخطاء سلوك رندرة React المجاور، فإن شرحنا حول لماذا يتخطى React Compiler مكوناً ما بصمت يغطي فئة مماثلة من الفشل الصامت، كما يوضح الدليل الخاص بـ التحميل المسبق (prefetching) باستخدام TanStack Query في Next.js App Router كيفية تسليم البيانات المجلوبة من السيرفر إلى العميل دون الحاجة إلى جلب مزدوج أو حدوث عدم تطابق.
الحواشي
-
مرجع React، "API hydrateRoot" — التحذيرات،
suppressHydrationWarning، الرندرة ثنائية المراحل (two-pass rendering)،identifierPrefixوonRecoverableError. https://React.dev/reference/React-dom/client/hydrateRoot ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 -
Next.js، "Text content does not match server-rendered HTML" — قائمة الأسباب المرقمة والحلول الموثقة الخاصة بإطار العمل. https://nextjs.org/docs/messages/React-hydration-error ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
فريق React، "React v19" — أقسام "Diffs for hydration errors" و "Compatibility with third-party scripts and extensions"، نُشر في 5 ديسمبر 2024. https://React.dev/blog/2024/12/05/React-19 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11
-
سجل npm،
Reactdist-taglatest— 19.2.8. https://registry.npmjs.org/React/latest ↩ -
سجل npm،
nextdist-taglatest— 16.3.0. https://registry.npmjs.org/next/latest ↩ -
reactjs/rfcs, "أخطاء الخادم في React 18" (RFC 0215) — كيف يقوم React بالتراجع إلى أقرب حدود Suspense عند حدوث تعارض في الـ hydration، والرجوع إلى عملية رندر نظيفة من جهة العميل من الجذر عند عدم وجود حدود. https://GitHub.com/reactjs/rfcs/blob/main/text/0215-server-errors-in-React-18.md ↩ ↩2
-
Next.js, منشور إصدار "Next.js 16.2" — مؤشر فرق الـ Hydration في تراكب أخطاء التطوير (dev error overlay). https://nextjs.org/blog/next-16-2 ↩
-
نقاش vercel/Next.js رقم 72035، "خطأ Hydration في Next.js 15 مع React 19 بسبب حقن سمة cz-shortcut-listen من إضافة Colorzilla"، تم فتحه في 29 أكتوبر 2024. https://GitHub.com/vercel/Next.js/discussions/72035 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
نقاش vercel/Next.js رقم 41816، "تحذير: سمات إضافية من الخادم: data-lt-installed". https://GitHub.com/vercel/Next.js/discussions/41816 ↩
-
React, مرجع API لـ "useSyncExternalStore" — بارامتر
getServerSnapshotوضماناته الخاصة بالخادم والـ hydration. https://React.dev/reference/React/useSyncExternalStore ↩ ↩2 ↩3 -
مشكلة nodejs/node رقم 46123، "تغيير جذري في تنسيق التاريخ/الوقت في إصدار Node 18.13 ICU 72" — قام ICU 72 بتغيير العديد من اللغات المحلية لإصدار U+202F حيث كان يتم استخدام مسافة عادية. https://GitHub.com/nodejs/node/issues/46123 ↩
-
Next.js، "كيفية التحميل المتأخر (lazy load) لمكونات العميل والمكتبات" (إصدار المستندات 16.3.0). https://nextjs.org/docs/app/guides/lazy-loading ↩ ↩2 ↩3
-
مرجع React لـ "useId" API — يوضح قسم التعمق "لماذا يعتبر
useIdأفضل من العداد المتزايد؟" أن المعرفات (IDs) تُشتق من مسار الأصل للمكون المستدعي، وتشير ملاحظة الأخطاء الشائعة إلى أنuseIdيتطلب شجرة مكونات متطابقة على الخادم والعميل. https://React.dev/reference/React/useId ↩ ↩2 -
فريق React، "React 19.2" — قسم "تحديث البادئة الافتراضية لـ
useId"، نُشر في 1 أكتوبر 2025. https://React.dev/blog/2025/10/01/React-19-2 ↩ ↩2 -
Cloudflare، "إيقاف ميزات API" — إدخال Auto Minify بتاريخ 2024-08-05، والذي يوقف دعم
GETوPATCH /zones/:zone_id/settings/minify. https://developers.cloudflare.com/fundamentals/API/reference/deprecations/ ↩ ↩2 -
مجتمع Cloudflare، "إيقاف Auto Minify" — موظفو Cloudflare حول إزالة واجهة المستخدم مقابل استمرار الوصول عبر API، أغسطس 2024. https://community.cloudflare.com/t/deprecating-auto-minify/655677/18 ↩ ↩2
-
Cloudflare، "إيقاف تشغيل Auto Minify عبر API". https://developers.cloudflare.com/speed/optimization/content/troubleshooting/disable-auto-minify/ ↩ ↩2 ↩3
