آلات الحالة المنتهية: أكثر أنماط التصميم إهمالًا في تطوير الواجهات
مقالة

آلات الحالة المنتهية: أكثر أنماط التصميم إهمالًا في تطوير الواجهات

أسامة الغانمي · 19 فبراير 2026

إذا كنت تستخدم useState لواجهة معقدة، فعلى الأرجح أنك تفعل ذلك بشكل خاطئ. هناك حل عمره 50 عامًا وأنت تتجاهله.

فخ المتغيرات المنطقية

هذا نمط مألوف:

typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function UserProfile() { const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const [isEditing, setIsEditing] = useState(false); const [isSaving, setIsSaving] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const handleSave = async () => { setIsSaving(true); setIsError(false); try { await saveUser(user); setIsEditing(false); } catch (e) { setIsError(true); setErrorMessage(e.message); } finally { setIsSaving(false); } }; // What combinations are valid? // isLoading=true, isError=true? // isEditing=true, isSaving=true? // Who knows! }

هذا ينتج 2^n حالة ممكنة (32 تركيبة لخمسة متغيرات منطقية). أغلبها غير صالح أو لا معنى له.

البديل: آلة الحالة

ماذا لو عرّفتَ الحالات الصالحة صراحةً؟

json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "states": [ { "name": "idle", "isInitial": true }, { "name": "loading" }, { "name": "editing" }, { "name": "saving" }, { "name": "error" } ], "events": ["FETCH", "EDIT", "SAVE", "SUCCESS", "ERROR", "CANCEL"], "transitions": [ { "from": "idle", "to": "loading", "event": "FETCH" }, { "from": "loading", "to": "idle", "event": "SUCCESS" }, { "from": "loading", "to": "error", "event": "ERROR" }, { "from": "idle", "to": "editing", "event": "EDIT" }, { "from": "editing", "to": "saving", "event": "SAVE" }, { "from": "saving", "to": "idle", "event": "SUCCESS" }, { "from": "saving", "to": "error", "event": "ERROR" }, { "from": "editing", "to": "idle", "event": "CANCEL" }, { "from": "error", "to": "idle", "event": "CANCEL" } ] }

الآن لدينا بالضبط 5 حالات و9 انتقالات صالحة. لا توجد تركيبات مستحيلة.

توضيح الفرق بصريًا

المتغيرات المنطقية: حالة متشابكة كالسباغيتي

text
1 2 3 4 5 isLoading=true / \ isError=true? isEditing=true? / \ ? ?

أي تركيبة ممكنة. تظهر الأخطاء من حالات غير صالحة لم تضعها في حسبانك.

آلة الحالة: رسم بياني موجّه

text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ┌─────────┐ ┌─────────►│ idle │◄────────┐ │ └────┬────┘ │ │ │ │ ERROR│ FETCH│ SUCCESS │ ▼ │ ┌────┴───┐ ┌─────────┐ │ │ error │ │ loading │ │ └───┬────┘ └────┬────┘ │ ▲ │ │ │ SUCCESS │ │ │ │ │ ▼ │ │ ┌─────────┐ │ └───────────┤ editing ├────────┘ └────┬────┘ │ SAVE ▼ ┌─────────┐ ┌─────────│ saving │─────────┐ │ └─────────┘ │ ERROR│ │SUCCESS │ │ └──────────────────────────────┘

كل مسار صريح. الانتقالات غير الصالحة غير موجودة.

مثال من الواقع: إرسال نموذج

الطريقة بالمتغيرات المنطقية

typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function ContactForm() { const [formData, setFormData] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [isSuccess, setIsSuccess] = useState(false); const [isError, setIsError] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const submit = async () => { setIsSubmitting(true); setIsError(false); setIsSuccess(false); try { await api.submit(formData); setIsSuccess(true); } catch (e) { setIsError(true); setErrorMessage(e.message); } finally { setIsSubmitting(false); } }; // Bug: What if isSuccess and isError are both true? // Bug: Can I submit again while isSubmitting? // Bug: What clears isSuccess? }

الطريقة بآلة الحالة

عرّف حالات وانتقالات صريحة:

json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 { "states": [ { "name": "editing", "isInitial": true }, { "name": "validating" }, { "name": "submitting" }, { "name": "success", "isTerminal": true }, { "name": "error" } ], "events": ["SUBMIT", "VALIDATED", "SUCCESS", "FAILURE", "RETRY", "EDIT"], "transitions": [ { "from": "editing", "to": "validating", "event": "SUBMIT" }, { "from": "validating", "to": "submitting", "event": "VALIDATED", "guard": ["=", "@validation.valid", true] }, { "from": "validating", "to": "editing", "event": "VALIDATED", "guard": ["=", "@validation.valid", false] }, { "from": "submitting", "to": "success", "event": "SUCCESS" }, { "from": "submitting", "to": "error", "event": "FAILURE" }, { "from": "error", "to": "editing", "event": "RETRY" } ] }

الفوائد:

  • ✅ لا يمكن الإرسال أثناء وجود إرسال جارٍ
  • ✅ التحقق يحدث في حالته الخاصة
  • ✅ الخطأ والنجاح يستبعد أحدهما الآخر
  • ✅ مسارات واضحة لإعادة المحاولة

إذا كنت تستخدم XState فهذا ينطبق عليه مباشرة. وإن كنت تفضّل نهجًا أخف، فحتى اتحاد مميَّز (discriminated union) بسيط في TypeScript يوصلك إلى معظم الطريق:

typescript
1 2 3 4 5 6 type FormState = | { status: 'editing' } | { status: 'validating' } | { status: 'submitting' } | { status: 'success' } | { status: 'error'; message: string };

لماذا يتجنب المطوّرون آلات الحالة

الخرافة 1: «إنها معقدة جدًا»

الواقع: المتغيرات المنطقية تبدو أبسط إلى أن يصبح عندك 5 منها أو أكثر. عندها تصير مصفوفة التفاعل بينها غير مفهومة.

الخرافة 2: «إنها للألعاب فقط»

الواقع: مطوّرو الألعاب يستخدمون آلات الحالة المنتهية لأنها تعمل. الواجهة تشبه اللعبة تمامًا: أفعال المستخدم تُحدث تغيّرات في الحالة.

الخرافة 3: «من الصعب تغييرها»

الواقع: تغيير آلة الحالة يعني إضافة حالة أو انتقال. أما تغيير المتغيرات المنطقية فيعني البحث بين سلاسل useEffect.

متى تستخدم آلات الحالة

السيناريوالمتغيرات المنطقيةآلة الحالة
2-3 حالات بسيطة✅ مقبول✅ أفضل
عمليات غير متزامنة❌ مليئة بالأخطاء✅ واضحة
تدفقات متعددة الخطوات❌ فوضوية✅ مثالية
أوضاع واجهة معقدة❌ مستحيلة✅ الخيار الأمثل

تشبيه من الواقع: إشارات المرور

إشارات المرور هي آلة الحالة النموذجية:

text
1 Red → Green → Yellow → Red

تخيّل لو أن إشارات المرور استخدمت متغيرات منطقية:

javascript
1 2 3 4 5 6 7 const [isRed, setIsRed] = useState(true); const [isGreen, setIsGreen] = useState(false); const [isYellow, setIsYellow] = useState(false); // Bug: All could be true! // Bug: All could be false! // Bug: Green could turn directly to Red!

مهندسو المرور يستخدمون آلات الحالة لأن الأرواح تعتمد على حالات يمكن التنبؤ بها.

وسلامة عقول مستخدميك تعتمد عليها أيضًا.

جرّبها: حوّل فوضى المتغيرات المنطقية

خذ هذا المكوّن الممتلئ بالمتغيرات المنطقية:

typescript
1 2 3 4 5 6 7 8 function Checkout() { const [isCartOpen, setIsCartOpen] = useState(false); const [isCheckingOut, setIsCheckingOut] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [isComplete, setIsComplete] = useState(false); const [hasError, setHasError] = useState(false); // ... nightmare of useEffect }

حوّله إلى حالات صريحة:

text
1 2 states: browsing → cartOpen → checkoutForm → processing → complete ↘ error → checkoutForm (retry)

نسخة آلة الحالة فيها 6 حالات صريحة بدل 32 تركيبة منطقية ممكنة.

الخلاصة

آلات الحالة المنتهية ليست تمارين أكاديمية، بل أدوات عملية لإدارة التعقيد.

  • 2-3 متغيرات منطقية: غالبًا لا بأس
  • 4 متغيرات منطقية أو أكثر: فكّر في آلة حالة
  • التدفقات غير المتزامنة: استخدم آلة حالة بلا تردد
  • واجهة متعددة الخطوات: آلة حالة أو لا شيء

مكتبات مثل XState تجعل ذلك سهلًا في أي مشروع React/Vue/Svelte.

أسامة الغانمي

المعرفة حرية. ومن حق الناس أن يفهموا الأنظمة التي تحكمهم.

في أماكن أخرى
© 2026 أسامة الغانمي. ليوبليانا، سلوفينيا.