إدارة الحالة في تطبيقات المؤسسات: الملكية قبل المكتبات
إدارة الحالة في تطبيقات و للمؤسسات: سمِّ نوع الحالة، وامنحها مالكاً واحداً، واشتقّ الباقي. و والرابط والنماذج.

يفتح موظف الدعم الرابط الذي أرسله عميل: صفحة حساب مصفّاة على فئة واحدة. فتعرض الصفحة كل الفئات، لأن التصفية كانت محفوظة في حقل داخل المكوّن، لا في الرابط (). لم يكتب أحد خللاً. لكن الحالة خُزّنت على أنها من النوع الخطأ.
هذه هي مشكلة الحالة (state) في تطبيقات المؤسسات. واختيار المكتبة يأتي أخيراً. فأولاً تقرّر نوع كل جزء من الحالة، ومن يملكه، وكم يعيش.
خمسة أنواع من الحالة
- بيانات الخادم. الحسابات، والمعاملات. تملكها الواجهة الخلفية، وتحتفظ الواجهة الأمامية بنسخة منها قد تتقادم (الجزء 3).
- حالة الواجهة. أي لوحة مفتوحة. يملكها مكوّن واحد، وتختفي حين يختفي.
- حالة النموذج. المسودة، والتحقق من صحتها، وهل عُدّلت.
- حالة الرابط. التصفية، والتبويبات، والسجل المحدد. كل ما يجب أن يستعيده رابط، أو تذكرة دعم، أو زر الرجوع.
- حالة الجلسة. المستخدم وأدواره. حالة عامة فعلاً.
من أخطاء الحالة الشائعة أن يُخزَّن نوع على أنه نوع آخر: بيانات خادم منسوخة داخل مكوّن، وتصفية في الذاكرة مكانها الرابط، وأدوار محفوظة في مخزن () لا يحدّثه أحد.
الرابط حالة
إذا كان من المحتمل أن يشاركه أحد، أو يحفظه في المفضلة، أو يعود إليه، فمكانه الرابط. في Angular، تربط withComponentInputBinding معاملات المسار بمدخلات المكوّن، ويضبط الموجّه افتراضياً المُدخل على undefined حين تغيب القيمة عن المسار، وهذا «يضمن عدم الاحتفاظ ببيانات قديمة». وتُربط معاملات الاستعلام بالطريقة نفسها، فيمكن للتصفية أن تعيش هناك أيضاً.
مخزن لكل مجال، ومخزن عام واحد على الأكثر
أفضّل أن يكون لكل مجال مخزنه الخاص: الحسابات، والبطاقات، والطلبات. يملك الفريق مخزنه كما يملك مكتبته. أما المخزن العام الواحد فيبدو مرتباً في اليوم الأول، ثم يتحول إلى المكان الذي تلتقي فيه حالة كل فريق بأخطاء كل فريق آخر.
ما يبقى عاماً هو ما يتقاطع مع الجميع بطبيعته: الجلسة، والصلاحيات، وربما مفاتيح الميزات (). وكل ما عدا ذلك يبقى قريباً من الميزة التي تملكه.
اشتقّ، ولا تكرّر
الرصيد الإجمالي ليس حالة، بل دالةٌ في الحسابات. يقولها Angular صراحة: «تجنّب استخدام التأثيرات لنشر تغييرات الحالة»، واستخدم الإشارات المحسوبة () بدلاً منها. ويقول React الشيء نفسه بكلماته: «حين يمكن حساب شيء من الخصائص أو الحالة الموجودة، فلا تضعه في الحالة. بل احسبه أثناء العرض.»
كل نسخة من قيمة مالكٌ ثانٍ لها. والمالكان سيختلفان.
النماذج الطويلة الخاضعة للتنظيم
طلب قرض أو استمارة استقبال مريض حالةٌ لها قواعد: مسودات تصمد أثناء استراحة الغداء، وتحقق يطابق تحقق الخادم، وتتبّع للتعديلات () كي لا تضيع على أحد ساعة من الإدخال. ويبقى التحقق على الخادم قائماً، أما فحص الواجهة فهو لراحة من يكتب.
في الكود الجديد، تُزامن نماذج الإشارات () في Angular نموذج البيانات مع حقوله تلقائياً. وفريق Angular يتوخّى الحذر هنا: فهي «تعمل على أفضل وجه في التطبيقات الجديدة المبنية بالإشارات»، و«تبقى النماذج التفاعلية خياراً متيناً» في تطبيق قائم يستخدمها. وفي قاعدة كود عمرها عشر سنوات، هذه هي النصيحة الصحيحة. لا ترحّل النماذج من أجل الموضة.
الإشارات أم RxJS
قاعدتي: الإشارات (signals) للحالة (قيمة الآن)، و RxJS للأحداث عبر الزمن (ضغطات المفاتيح، و ، وإعادة المحاولات). والجسر بينهما موجود: تحوّل toSignal كائن Observable إلى إشارة، و«تتصرف بشكل مشابه لـ async pipe في القوالب». وتناسب الإشارات أيضاً Angular دون ()، حيث يكون «تحديث إشارة تُقرأ في قالب» أحد الإشعارات التي تجدول اكتشاف التغييرات.
في Atlas Bank
يحتفظ AccountsStore في libs/data-access/banking بحالته خاصة، ويوزّع إشارات للقراءة فقط وقيماً مشتقة:
private readonly accountsState = signal<readonly Account[]>([]);
private readonly transactionsState = signal<readonly Transaction[]>([]);
private readonly beneficiariesState = signal<readonly Beneficiary[]>([]);
private readonly loadingState = signal(false);
private readonly loadedState = signal(false);
private inFlight?: Promise<void>;
readonly accounts = this.accountsState.asReadonly();
readonly transactions = this.transactionsState.asReadonly();
readonly beneficiaries = this.beneficiariesState.asReadonly();
readonly loading = this.loadingState.asReadonly();
readonly loaded = this.loadedState.asReadonly();
readonly totalBalanceMad = computed(() =>
this.accountsState()
.filter((a) => a.status !== 'closed')
.reduce((sum, a) => sum + a.balanceMad, 0),
);تكشف asReadonly() القيمة «دون السماح بتعديلها مباشرة»، فيبقى المخزن المالك الوحيد. وتحصل صفحة تفاصيل الحساب على id من المسار عبر withComponentInputBinding، وتشتق القائمة المصفّاة بـ computed. أما تصفية الفئات فيها فهي إشارة داخل المكوّن. لا بأس بذلك في تطبيق تجريبي؛ أما في مسار الدعم لدى بنك، فسأنقلها إلى معامل استعلام (query parameter) في الرابط.
وتُظهر شاشة التحويل القديمة الشكل القديم: حقول عادية، واشتراكات يدوية، ثم markForCheck() بعدها. ويصف ملف AGENTS.md في المستودع ذلك بأنه «علامة على مشكلة تستدعي الترحيل، لا نمط يُنسخ».
ما تركته جانباً، ولماذا
- المقارنات بين و و و . جولة على المكتبات. الملكية تقرّر أولاً، وأيّ منها يستطيع حمل النتيجة.
- عدم القابلية للتغيير () بوصفه مفهوماً. متضمَّن في الإشارات المخصّصة للقراءة فقط وفي القيم المشتقة.
- تخزين الأحداث () على جهة العميل. نادر في الواجهات الأمامية، وهو شكل يخص الواجهة الخلفية.
- الحالة الذرّية (). مفردات مكتبات React للفكرة نفسها: «اشتقّ، ولا تكرّر».
مع بداية الأسبوع
- اختر شاشة واحدة وصنّف كل جزء من حالتها حسب نوعه: خادم، أو واجهة، أو نموذج، أو رابط، أو جلسة.
- ابحث عن قيمة واحدة مخزّنة مرتين. واجعل إحداهما
computed(أو عملية حساب أثناء العرض). - ابحث عن تصفية واحدة لا تستطيع تذكرة دعم إعادة إنتاجها. وانقلها إلى الرابط.
التالي: الجزء الثالث، عن الشبكة والتخزين المؤقت.
لوكيل البرمجة الخاص بك
قواعد هذا المقال، مكتوبة لوكيل. الصقها في ملف AGENTS.md لديك، أو احفظها مهارةً ()، أو وجّه وكيلك إلى الملف.
# State rules for enterprise apps Source: https://tt-tech.io/blog/enterprise-frontend-state-ownership For Angular and React frontends. Add to AGENTS.md or save as a skill. Your project's own rules win where they differ. ## Rules 1. Before you add state, name its kind: server data, UI state, form state, URL state or session state. Store it as that kind. 2. Put anything a link, a bookmark, a support ticket or the back button must restore in the URL: filters, tabs, the selected record. In Angular, bind route and query parameters to inputs with `withComponentInputBinding`, and expect a missing parameter to arrive as `undefined`. 3. Server data belongs to the backend. Never copy it into a component. Read it from the store of its domain. 4. Give each domain its own store (accounts, cards, requests), owned by the team that owns the domain. Never add a single global store for everything. 5. Keep global only what is cross-cutting by nature: the session, the permissions, maybe feature flags. Do not cache roles in a store nobody refreshes. 6. Keep UI state (which panel is open) in the component that owns it. 7. Derive, do not duplicate. If a value can be calculated from state, use `computed` in Angular or calculate it during render in React. Never store it as a second copy. 8. Never use `effect` to propagate state changes. Use `computed` instead. 9. Keep a store's writable state private. Expose read-only signals with `asReadonly()` and derived values, so the store is the only owner. 10. In long regulated forms, keep drafts, mirror the server's validation and track dirty state. The server still validates; the client check is for the person typing. 11. Use Signal Forms in new code built with signals. Keep reactive forms in an existing app that uses them. Do not migrate forms for fashion. 12. Use signals for state (a value now) and RxJS for events over time (keystrokes, websockets, retries). Bridge them with `toSignal`. ## Before you say done - For every new piece of state in the diff, list its kind and its owner in the pull request. - Search the diff for `effect(` calls that set other state, and for stored values that could be a `computed` or a calculation during render. - For every new filter, tab or selection, confirm it lives in the URL, or say why not.
المصادر
- Angular, Other routing tasks, التوثيق
- Angular, Side effects for non-reactives APIs, التوثيق
- React, You Might Not Need an Effect, التوثيق
- Angular, Forms with Angular Signals, التوثيق
- Angular, RxJS interop with Angular signals, التوثيق
- Angular, Angular without ZoneJS (Zoneless), التوثيق
- Angular, Angular Signals, التوثيق