إدارة الحالة صعبة لأن التطبيقات تتعامل مع مصادر حقيقة متعددة، بيانات غير متزامنة، تداخلات واجهة المستخدم، ومقايضات الأداء. تعلّم أنماطًا لتقليل الأخطاء.

في تطبيق الواجهة الأمامية، الحالة هي ببساطة البيانات التي تعتمد عليها واجهة المستخدم والتي يمكن أن تتغير مع الزمن.
عندما تتغير الحالة، يجب أن يتحدّث العرض ليطابقها. إذا لم يتحدّث العرض، أو حدثت تحديثات بشكل غير متسق، أو ظهر مزيج من القيم القديمة والجديدة، فستشعر فوراً بـ"مشاكل الحالة" — أزرار تظل معطلة، إجماليات لا تطابق، أو واجهة لا تعكس ما فعله المستخدم للتو.
تظهر الحالة في تفاعلات صغيرة وكبيرة، مثل:
بعض هذه الحالات "مؤقتة" (مثل تبويب محدد)، بينما تبدو أخرى "مهمة" (مثل السلة). كلها حالة لأنها تؤثر على ما تعرضه الواجهة الآن.
المتغير البسيط يهم فقط حيث وُضع. الحالة مختلفة لأنها لها قواعد:
الهدف الحقيقي لإدارة الحالة ليس تخزين البيانات—إنما جعل التحديثات متوقعة حتى تبقى الواجهة متناسقة. عندما تستطيع الإجابة عن "ما الذي تغيّر، ومتى، ولماذا"، تصبح الحالة قابلة للإدارة. عندما لا تستطيع، تتحول حتى الميزات البسيطة لمفاجآت.
في بداية مشروع الواجهة الأمامية، تبدو الحالة مملة تقريبًا—بمعنى جيد. لديك مكوّن واحد، إدخال واحد، وتحديث واضح. يكتب المستخدم في حقل، تحفظ القيمة، وتعيد الواجهة الرسم. كل شيء مرئي وفوري ومحدود.
تخيل إدخال نصي واحد يعرض ما كتبته مباشرة:
في هذا الإعداد، الحالة في الأساس: متغير يتغير مع الزمن. يمكنك الإشارة إلى مكان تخزينه ومكان تحديثه، وتنتهي المهمة.
السبب أن الحالة المحلية تعمل لأن النموذج الذهني يطابق بنية الكود:
حتى لو استخدمت إطارًا مثل React، لا تحتاج للتفكير بعمق في الهندسة. الإعدادات الافتراضية تكفي.
بمجرد أن يتوقف التطبيق عن كونه "صفحة مع عنصر" ويصبح "منتجًا"، تتوقف الحالة عن العيش في مكان واحد.
الآن قد تحتاج نفس قطعة البيانات عبر:
قد يظهر اسم الملف الشخصي في رأس الصفحة، يُحرّر في صفحة الإعدادات، يُخزّن مؤقتًا للتحميل الأسرع، ويستخدم أيضًا لتخصيص رسالة ترحيب. فجأة، السؤال ليس "كيف أخزن هذه القيمة؟" بل "أين يجب أن تعيش هذه القيمة لكي تظل صحيحة في كل مكان؟"
تعقيد الحالة لا يزداد تدريجيًا مع الميزات—بل يقفز.
إضافة مكان ثانٍ يقرأ نفس البيانات ليست "أصعب بضعف". إنها تُدخل مشاكل تنسيقية: الحفاظ على اتساق العرض، منع القيم القديمة، تقرير من يحدث ماذا، والتعامل مع التوقيت. بمجرد أن تملك عدة قطع حالة مشتركة بالإضافة للعمل غير المتزامن، قد تنتهي بسلوك صعب الفهم—مع أن كل ميزة فردية تبدو بسيطة بمفردها.
تصبح الحالة مؤلمة عندما يُخزن نفس "الحقيقة" في أكثر من مكان. كل نسخة يمكن أن تنحرف، والآن واجهتك تتشاجر مع نفسها.
تنتهي معظم التطبيقات بعدة أماكن يمكن أن تمتلك "الحقيقة":
كل هذه أماكن صحيحة لامتلاك بعض الحالات. المشكلة تبدأ عندما تحاول كلها امتلاك نفس الحالة.
نمط شائع: تجلب بيانات الخادم، ثم تنسخها إلى حالة محلية "لكي نحرّرها". على سبيل المثال، تحمّل ملف تعريف المستخدم وتضبط formState = userFromApi. لاحقًا، يعيد الخادم الجلب (أو يحدّثه تبويب آخر)، والآن لديك نسختان: الكاش يقول شيئًا، والنموذج يقول شيئًا آخر.
يتسلّل التكرار أيضًا عبر التحويلات "المفيدة": تخزين كل من items وitemsCount، أو تخزين selectedId وselectedItem معًا.
عندما توجد مصادر متعددة للحقيقة، تميل الأخطاء لأن تبدو كالتالي:
لكل قطعة حالة، اختر مالكًا واحدًا—المكان الذي تُجرى فيه التحديثات—وعامل كل شيء آخر كـإسقاط (للقراءة فقط، مشتق، أو متزامن باتجاه واحد). إذا لم تستطع الإشارة إلى المالك، فربما تخزن نفس الحقيقة مرتين.
الكثير من حالة الواجهة الأمامية تبدو بسيطة لأنها متزامنة: ينقر المستخدم، تضبط قيمة، تُعاد الواجهة. الآثار الجانبية تكسر تلك القصة المتسلسلة.
الآثار الجانبية هي أي إجراءات تتجاوز نموذج "العرض النقي بناءً على البيانات" للمكوّن:
كل واحد منها يمكن أن يعمل لاحقًا، يفشل بشكل غير متوقع، أو يعمل أكثر من مرة.
التحديثات غير المتزامنة تُدخل الزمن كمتغير. لم تعد تفكر في "ما الذي حدث" بل في "ما الذي قد لا يزال يحدث". يمكن أن تتداخل طلبتان. يمكن أن تأتي استجابة بطيئة بعد واحدة أحدث. قد يتم إلغاء تركيب المكوّن بينما يحاول رد نداء غير متزامن تحديث الحالة.
لهذا تبدو الأخطاء غالبًا مثل:
بدلاً من رشّ منطق isLoading في أنحاء الواجهة، عامل العمل غير المتزامن كآلة حالة صغيرة:
تتبع البيانات و الحالة معًا، واحتفظ بمعرف (مثل معرف الطلب أو مفتاح الاستعلام) حتى تتمكن من تجاهل الاستجابات المتأخرة. هذا يجعل السؤال "ماذا يجب أن تعرض الواجهة الآن؟" قرارًا واضحًا، لا تخمينًا.
تبدأ كثير من مشاكل الحالة بسوء خلط بسيط: معاملة "ما يفعله المستخدم الآن" بنفس طريقة "ما يقوله الخادم أنه صحيح". كلاهما يمكن أن يتغير مع الزمن، لكن لهما قواعد مختلفة.
حالة الواجهة مؤقتة وتقودها التفاعلات. توجد لعرض الشاشة كما يتوقع المستخدم في هذه اللحظة.
من الأمثلة: النوافذ المنبثقة المفتوحة/المغلقة، الفلاتر النشطة، مسودة مدخل البحث، hover/focus، التبويب المختار، واجهة الترقيم (الصفحة الحالية، حجم الصفحة، موضع التمرير).
هذه الحالة عادة محلية لصفحة أو شجرة مكوّنات. لا بأس أن تُعاد إلى الحالة الافتراضية عند التنقل بعيدًا.
حالة الخادم هي بيانات من API: ملفات المستخدمين، قوائم المنتجات، الأذونات، الإشعارات، الإعدادات المحفوظة. هي "الحقيقة البعيدة" التي يمكن أن تتغير دون أن يفعل شيء في واجهتك (شخص آخر يحررها، الخادم يعيد حسابها، مهمة خلفية تُحدّثها).
لأنها بعيدة، فهي تحتاج أيضًا بيانات وصفية: حالات التحميل/الخطأ، طوابع زمنية للكاش، إعادة المحاولة، وإبطال الصلاحية.
إذا خزنت مسودات الواجهة داخل بيانات الخادم، فإن إعادة الجلب قد تمحو التحسينات المحلية. إذا خزنت استجابات الخادم داخل حالة الواجهة بدون قواعد كاش، ستصارع البيانات القديمة، الطلبات المكررة، والشاشات غير المتناسقة.
نمط فشل شائع: المستخدم يحرر نموذجًا بينما ينتهي جلب خلفي ويكتب الاستجابة الواردة فوق المسودة.
اتحكم في حالة الخادم بأنماط الكاش (جلب، كاش، إبطال، إعادة جلب عند التركيز) واعتبرها مشتركة وغير متزامنة.
اتحكم في حالة الواجهة بأدوات واجهة المستخدم (حالة المكوّن المحلية، السياق للمسائل UI المشتركة فعلًا)، واحفظ المسودات منفصلة حتى تقرر صراحة "الحفظ" إلى الخادم.
الحالة المشتقة هي أي قيمة يمكنك حسابها من حالة أخرى: إجمالي السلة من عناصر السلة، قائمة مُرشّحة من القائمة الأصلية + نص البحث، أو علامة canSubmit من قيم الحقول وقواعد التحقق.
من المغري تخزين هذه القيم لأن ذلك يبدو مريحًا ("سأخزن total أيضًا"). لكن بمجرد أن تتغير المدخلات في أكثر من مكان، تخاطر بالانحراف: total المخزن لا يطابق العناصر، القائمة المصفّاة لا تعكس الاستعلام الحالي، أو زر الإرسال يظل معطلًا بعد إصلاح خطأ. هذه الأخطاء مزعجة لأن لا شيء يبدو "خاطئًا" بمعزل—كل متغير حالة صحيح بمفرده، لكنه غير متسق مع الباقي.
النمط الآمن هو: خزّن الحد الأدنى من مصادر الحقيقة، واحسب كل شيء آخر عند القراءة. في React يمكن أن يكون ذلك دالة بسيطة، أو حسابًا مذكورًا (memoized).
const items = useCartItems();
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
const filtered = products.filter(p => p.name.includes(query));
في التطبيقات الأكبر، تُطبّق "المنتقيات" (selectors) هذه الفكرة رسميًا: مكان واحد يعرّف كيف يُشتق total وfilteredProducts وvisibleTodos، وكل مكون يستخدم نفس المنطق.
الحساب عند كل إعادة رسم عادةً ما يكون جيدًا. خزّن فقط عندما تقيس تكلفة حقيقية: تحويلات مكلفة، قوائم ضخمة، أو قيم مشتقة مشتركة عبر مكونات كثيرة. استخدم التذكّر (useMemo، تذكّر في المنتقيات) بحيث تكون مفاتيح الكاش هي المدخلات الحقيقية—وإلا فستعود لمشكلة الانحراف، لكن تحت عنوان الأداء.
تصبح الحالة مؤلمة عندما يكون غير واضح من يملكها.
مالك قطعة الحالة هو المكان في التطبيق الذي له الحق في تحديثها. قد تقرأها أجزاء أخرى من الواجهة (عبر الخصائص، السياق، المنتقيات)، لكن لا يجب أن تغيّرها مباشرة.
الملكية الواضحة تجيب على سؤالين:
عندما تتلاشى هذه الحدود، تحصل على تحديثات متضاربة، لحظات "لماذا تغيّر هذا؟"، ومكونات يصعب إعادة استخدامها.
وضع الحالة في متجر عالمي (أو سياق على مستوى أعلى) قد يبدو نظيفًا: يمكن لأي شيء الوصول إليها، وتتجنب تمرير الخصائص عبر كثير من المستويات. المقايضة هي الترابط غير المقصود—فجأة الشاشات غير المتعلقة تعتمد على نفس القيم، والتغييرات الصغيرة تتسلّل إلى كل التطبيق.
الحالة العالمية مناسبة للأشياء المشتركة فعلاً، مثل جلسة المستخدم الحالية، علامات الميزات على مستوى التطبيق، أو قائمة إشعارات مشتركة.
نمط شائع هو البدء محليًا ثم "رفع" الحالة إلى أقرب أصل مشترك فقط عندما يحتاج شقيّان للتنسيق.
إذا كان مكوّن واحد فقط يحتاج الحالة، احتفظ بها هناك. إذا احتاجتها عدة مكونات، ارفعها إلى أصغر صاحب مشترك. إذا احتاجها العديد من المناطق البعيدة، حينها فكر في العالمية.
أبقِ الحالة قريبة من مكان استخدامها ما لم يكن المشاركة ضرورية.
هذا يجعل المكونات أسهل للفهم، يقلل الاعتماديات العرضية، ويجعل إعادة الهيكلة في المستقبل أقل رهبة لأن عدد الأجزاء المسموح لها بتغيير نفس البيانات أقل.
تبدو تطبيقات الواجهة الأمامية "خيطية"، لكن إدخالات المستخدم، المؤقتات، الرسوم المتحركة، وطلبات الشبكة كلها تعمل بشكلٍ مستقل. هذا يعني أن تحديثات متعددة يمكن أن تكون في الجوّ ولا تنتهي بالضرورة بنفس ترتيب بدايتها.
تصادم شائع: جزآن من الواجهة يحدثان نفس الحالة.
query مع كل ضغطة مفتاح.query (أو نفس قائمة النتائج) عند التغيير.كل تحديث بمفرده صحيح. معًا، يمكن أن يكتب أحدهما فوق الآخر حسب التوقيت. الأسوأ من ذلك، قد تعرض نتائج لاستعلام سابق بينما الواجهة تظهر الفلاتر الجديدة.
تظهر ظروف السباق عندما تطلق الطلب A ثم بسرعة الطلب B—لكن الطلب A يعود أخيرًا.
مثال: المستخدم يكتب "c"، "ca"، "cat". إذا كان طلب "c" بطيئًا وطلب "cat" سريعًا، قد تعرض الواجهة مؤقتًا نتائج "cat" ثم تُستبدل بنتائج "c" القديمة عند وصولها.
الخطأ دقيق لأن كل شيء "عمل"—فقط بترتيب خاطئ.
عمومًا تريد واحدة من الاستراتيجيات التالية:
AbortController).نهج معرف الطلب البسيط:
let latestRequestId = 0;
async function fetchResults(query) {
const requestId = ++latestRequestId;
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
if (requestId !== latestRequestId) return; // stale response
setResults(data);
}
التحديثات التفاؤلية تجعل الواجهة تبدو فورية: تحدث الشاشة قبل تأكيد الخادم. لكن التزامن يمكن أن يكسر الافتراضات:
لحفظ التفاؤل بأمان، تحتاج عادةً قاعدة مصالحة واضحة: تتتبع الإجراء المعلق، تطبق استجابات الخادم بالترتيب، وإذا اضطررت للتراجع فالتراجع إلى نقطة تحقق معروفة (ليس "ما تبدو عليه الواجهة الآن").
تحديثات الحالة ليست "مجانية". عندما تتغير الحالة، يجب على التطبيق تحديد أي أجزاء من الشاشة قد تتأثر ثم القيام بالعمل لتعكس الواقع الجديد: إعادة حساب القيم، إعادة رسم الواجهة، إعادة تشغيل منطق التنسيق، وأحيانًا إعادة الجلب أو التحقق من الصحة. إذا كانت سلسلة التبع هذه أكبر مما يجب، يشعر المستخدم بالتلعثم أو التأخر.
قد يتسبب تبديل واحد بسيط في تشغيل عمل زائد:
النهاية ليست تقنية فقط—إنها تجربة: الكتابة تبدو متأخرة، الرسوم تتلعثم، والواجهة تفقد شعور الاستجابة السريعة المرتبط بالمنتجات المصقولة.
أحد الأسباب الشائعة هو الحالة التي تكون "عريضة جدًا": كائن واحد كبير يحمل معلومات غير مرتبطة. تحديث أي حقل يجعل الدلو كله يبدو جديدًا، فتصحو أجزاء أكثر من الواجهة مما يلزم.
فخ آخر هو تخزين القيم المحسوبة في الحالة وتحديثها يدويًا. هذا غالبًا يخلق تحديثات إضافية (وعمل واجهة إضافي) لمجرد الحفاظ على التناسق.
قسّم الحالة إلى شرائح أصغر. أبقِ المخاوف غير المرتبطة منفصلة حتى لا يتسبب تغيير حقل بحث في تحديث صفحة كاملة من النتائج.
نمذج البيانات (normalize). بدلاً من تخزين نفس العنصر في أماكن عديدة، خزّنه مرة واحدة وارجع له عبر مرجع. هذا يقلّل التحديثات المكررة ويمنع "عواصف التغيير" حيث يضطر التطبيق لإعادة كتابة نسخ عديدة.
ذكّر القيم المشتقة. إذا كانت قيمة تُحسب من حالة أخرى (مثل النتائج المصفاة)، خزّن نتيجة هذا الحساب بحيث تُعاد حسابها فقط عندما تتغير المدخلات فعليًا.
إدارة الحالة المراعية للأداء تدور حول الحصر: يجب أن تؤثر التحديثات على أصغر منطقة ممكنة، ويجب أن يحدث العمل المكلف فقط عند الحاجة الحقيقية. عندما تتحقق هذه الشروط، يتوقف المستخدمون عن ملاحظة الإطار ويبدأون في الوثوق بالواجهة.
تبدو أخطاء الحالة شخصية أحيانًا: الواجهة "خاطئة"، لكن لا يمكنك الإجابة عن أبسط سؤال—من غير هذه القيمة ومتى؟ إذا تغير رقم، اختفى لافتة، أو تعطّل زر، تحتاج جدولًا زمنيًا، لا حدسًا.
أسرع طريق للوضوح هو نموذج تحديث متوقع. سواء استخدمت reducers، أحداث، أو متجرًا، اهدف إلى نمط حيث:
setShippingMethod('express')، وليس updateStuff)سجلات الأفعال الواضحة تحول التصحيح من "التحديق في الشاشة" إلى "اتّباع الإيصال". حتى سجلات الكونسول البسيطة (اسم الفعل + الحقول الرئيسة) تتفوق على محاولة إعادة بناء ما حدث من الأعراض.
لا تحاول اختبار كل إعادة رسم. بدلًا من ذلك، اختبر الأجزاء التي ينبغي أن تتصرف كمنطق نقي:
هذا الخليط يكتشف أخطاء "الرياضيات" ومشاكل الربط في العالم الحقيقي.
تختبئ المشاكل غير المتزامنة في الفجوات. أضف بيانات وصفية بسيطة تجعل الجداول الزمنية مرئية:
حين يكتب رد متأخر فوق أحدث، يمكنك إثبات ذلك فورًا—والإصلاح بثقة.
اختيار أداة الحالة يصبح أسهل عندما تعاملها كنتيجة لقرارات التصميم، لا كنقطة بداية. قبل مقارنة المكتبات، ارسم حدود الحالات: ما الذي محلي للمكوّن، ما الذي يجب مشاركته، وما الذي هو فعليًا "بيانات خادم" تجلبها وتزامنها.
طريقة عملية لاتخاذ القرار هي النظر إلى بعض القيود:
إذا بدأت بـ"نستخدم X في كل مكان" ستخزن الأشياء الخاطئة في الأماكن الخاطئة. ابدأ بـالملكية: من يحدث هذه القيمة، من يقرأها، وماذا يجب أن يحدث عند تغيّرها.
تتعامل كثير من التطبيقات بشكل جيد مع مكتبة حالة خادم لبيانات الـ API زائد حل صغير لحالة واجهة المستخدم للمخاوف العميلية فقط مثل النوافذ المنبثقة، الفلاتر، أو مسودات النماذج. الهدف هو الوضوح: يعيش كل نوع من الحالات حيث يكون من الأسهل التفكير فيه.
إذا كنت تتكرر على حدود الحالة وتدفّقات غير المتزامن، يمكن أن يسرّع Koder.ai حلقة "جرب، لاحظ، حسّن". لأنه يولد واجهات React (وخوادم Go + PostgreSQL) من محادثة مع سير عمل قائم على وكلاء، يمكنك بناء نماذج أولية لنماذج ملكية بدائل بسرعة (محلي مقابل عالمي، كاش الخادم مقابل مسودات الواجهة)، ثم تحتفظ بالإصدار الذي يظل متوقعًا.
ميزتان عمليتان تساعدان عند التجريب في الحالة: وضع التخطيط (لتخطيط نموذج الحالة قبل البناء) واللقطات + التراجع (لاختبار عمليات إعادة الهيكلة بأمان مثل "إزالة الحالة المشتقة" أو "إضافة معرفات الطلب" دون فقدان نقطة عمل).
تصبح الحالة أسهل عندما تتعامل معها كمشكلة تصميم: قرّر من يملكها، ماذا تمثل، وكيف تتغير. استخدم هذه القائمة عندما يبدأ مكوّن بالشعور "غامضًا."
اسأل: أي جزء من التطبيق مسؤول عن هذه البيانات؟ ضع الحالة قريبة قدر الإمكان من مكان استخدامها، وارفعها فقط عندما يحتاج عدة أجزاء للتنسيق.
إذا كان بإمكانك حساب شيء من حالة أخرى، فلا تخزنه.
items, filterText).visibleItems) أثناء العرض أو عبر التذكّر.العمل غير المتزامن أوضح عندما تنمذجه مباشرة:
status: 'idle' | 'loading' | 'success' | 'error'، مع data وerror.isLoading, isFetching, isSaving, hasLoaded, …) بدلًا من حالة واحدة.اسعَ إلى أخطاء أقل من نوع "كيف وصلت إلى هذه الحالة؟"، تغييرات لا تتطلّب لمس خمسة ملفات، ونموذج ذهني تستطيع فيه الإشارة إلى مكان واحد وقول: هنا تعيش الحقيقة.