تتعامل معظم أكواد الواجهة الأمامية (frontend) مع العمل غير المتزامن (asynchronous work) كأنه قالب واحد. تقوم ببدء Promise وتنتظر اكتماله، ثم تضع النتيجة في الحالة المحلية (local state)، وتترك الإطار (framework) يقوم بمطابقة الفروقات (reconcile the diff). هذا النمط مغرٍ لأنه يعمل في كل مكان: طلب REST، أو إرسال نموذج، أو رسالة WebSocket. جميعها تنتهي في نفس useEffect أو معالج الأحداث، وتمر جميعها عبر setState كأنها مجرد تمديدات برمجية غير متزامنة متطابقة. هذا التوحيد هو فخ. في التطبيقات الحقيقية، ليس كل العمل غير المتزامن متشابهاً. التظاهر بذلك يحول مكونات واجهة المستخدم الخاصة بك إلى مهندسي بيانات عن طريق الخطأ، يتم رقعهم باستخدام useEffect hooks وبالتمني فقط.
في الواقع، تنقسم العمليات غير المتزامنة إلى ثلاثة أنواع مختلفة. لكل منها علاقة مختلفة مع الوقت، والتخزين المؤقت (caching)، والملكية (ownership). تعلم التمييز بينها هو ما يحافظ على سرعة الواجهة الأمامية، وصحتها، واستقرارها.
الاستعلامات (Queries): حقائق ذات عنوان
الاستعلام ليس مجرد عملية جلب (fetch). إنه طلب لحقيقة يمكن تحديدها. أنت تطلب /user/123 وليس "بعض بيانات المستخدم". هذا التمييز مهم لأن الهوية هي ما يجعل التخزين المؤقت ممكناً. إذا احتاج مكونان على نفس الشاشة إلى سجل المستخدم نفسه، فيجب أن يتشاركا إجابة واحدة. عندما يحتفظ كل مكون بنسخته المحلية الخاصة في useState ، فإنك تشتت الحقيقة. سيصبح رمز المستخدم (avatar) في رأس الصفحة والاسم في الشريط الجانبي غير متطابقين لأنهما جُلبا في أوقات مختلفة، أو لأن أحدهما فشل بينما نجح الآخر.
فكر في الاستعلام كمورد (resource)، وليس كإجراء (action). له مفتاح تخزين مؤقت (cache key)، وسياسة صلاحية (freshness policy)، ودورة حياة (lifecycle) تدوم أطول من أي مكون منفرد. طبقة الاستعلام المصممة جيداً تدرك أن قراءة /projects?page=2 تختلف عن قراءة /projects?page=3. كل رابط (URL) ومجموعة معاملات (parameters) تشكل عنواناً، والبيانات الموجودة في ذلك العنوان قد تكون قديمة، أو حديثة، أو مفقودة. لا ينبغي لواجهة المستخدم أن تتولى هذه العمليات الحسابية. يجب أن تطلب طبقة البيانات user:123 وتتلقى لقطة (snapshot). وسواء جاءت تلك اللقطة من الخادم قبل ثانيتين أو من التخزين المؤقت قبل مللي ثانية واحدة، فهذا ليس من شأن المكون.
النتائج العملية لذلك فورية. عندما تعامل كل عملية قراءة كأنها جلب أمر (imperative fetch) داخل المكون، فإنك تفقد ميزة إزالة التكرار (deduplication). وتفقد التحديث في الخلفية (background refresh). وتفقد القدرة على عرض البيانات المخزنة مؤقتاً فوراً أثناء التحقق منها في الخلفية. الاستعلام يستحق مكاناً خارج شجرة واجهة المستخدم الخاصة بك.
التعديلات (Mutations): تغيير العالم
إذا كانت الاستعلامات تسأل عن العالم، فإن التعديلات تغيره. طلب الشبكة الفعلي — POST أو PUT أو DELETE — عادة ما يكون الجزء السهل. الجزء الصعب هو كل ما يحدث بعد أن يقول الخادم "OK".
افترض أن مستخدماً قام بتحديث اسمه المستعار. التعديل نفسه هو طلب واحد. لكن نطاق التأثير (blast radius) موجود في كل مكان. صفحة الملف الشخصي تحمل الاسم القديم. شريط التنقل يعرض الاسم القديم. قد يشير سجل التعليقات إليه. إذا كان كود التعديل الخاص بك لا يفعل شيئاً سوى تبديل علامة isLoading محلية ثم تحديث جزء واحد من الحالة، فإن تطبيقك الآن يكذب على نفسه. بعض زوايا واجهة المستخدم تتظاهر بأن التغيير قد حدث، بينما ليس لدى البعض الآخر أي فكرة عن حدوث أي تغيير على الإطلاق.
يجب أن يعلن التعديل عن تأثيره على مخطط البيانات (data graph). يجب أن يخبر النظام أي الاستعلامات أصبحت الآن غير صالحة، وأي مفاتيح مخزنة مؤقتاً تحتاج إلى إعادة جلب، وأي علاقات قد تغيرت. هذا يختلف جوهرياً عن الاستعلام. الاستعلام للقراءة فقط وقابل للمشاركة. أما التعديل فهو يركز على الكتابة ومدمر لمخازن البيانات المؤقتة الموجودة. دمجهم في تجريد (abstraction) واحد يعني أن المطورين سينتهي بهم الأمر باستدعاء refetch() يدوياً داخل مكونات عشوائية، أو والأسوأ من ذلك، نثر useEffect hooks عبر الشجرة "لمزامنة" الحالة المحلية وإعادتها لتتوافق مع الخادم.
نموذج الملكية مختلف أيضاً. عادة ما تكون الاستعلامات مملوكة للتخزين المؤقت. أما التعديل فيكون مملوكاً لإجراء المستخدم الذي أطلقه. وله حالة انتظار (pending state)، وحالة خطأ (error state)، وربما قيمة متفائلة (optimistic value) تحتاج إلى التراجع
