
إذا كنت تستخدم useState لواجهة معقدة، فعلى الأرجح أنك تفعل ذلك بشكل خاطئ. هناك حل عمره 50 عامًا وأنت تتجاهله.
هذا نمط مألوف:
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 تركيبة لخمسة متغيرات منطقية). أغلبها غير صالح أو لا معنى له.
ماذا لو عرّفتَ الحالات الصالحة صراحةً؟
{
"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 انتقالات صالحة. لا توجد تركيبات مستحيلة.
isLoading=true
/ \
isError=true? isEditing=true?
/ \
? ?أي تركيبة ممكنة. تظهر الأخطاء من حالات غير صالحة لم تضعها في حسبانك.
┌─────────┐
┌─────────►│ idle │◄────────┐
│ └────┬────┘ │
│ │ │
ERROR│ FETCH│ SUCCESS
│ ▼ │
┌────┴───┐ ┌─────────┐ │
│ error │ │ loading │ │
└───┬────┘ └────┬────┘ │
▲ │ │
│ SUCCESS │
│ │ │
│ ▼ │
│ ┌─────────┐ │
└───────────┤ editing ├────────┘
└────┬────┘
│ SAVE
▼
┌─────────┐
┌─────────│ saving │─────────┐
│ └─────────┘ │
ERROR│ │SUCCESS
│ │
└──────────────────────────────┘كل مسار صريح. الانتقالات غير الصالحة غير موجودة.
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?
}عرّف حالات وانتقالات صريحة:
{
"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 يوصلك إلى معظم الطريق:
type FormState =
| { status: 'editing' }
| { status: 'validating' }
| { status: 'submitting' }
| { status: 'success' }
| { status: 'error'; message: string };الواقع: المتغيرات المنطقية تبدو أبسط إلى أن يصبح عندك 5 منها أو أكثر. عندها تصير مصفوفة التفاعل بينها غير مفهومة.
الواقع: مطوّرو الألعاب يستخدمون آلات الحالة المنتهية لأنها تعمل. الواجهة تشبه اللعبة تمامًا: أفعال المستخدم تُحدث تغيّرات في الحالة.
الواقع: تغيير آلة الحالة يعني إضافة حالة أو انتقال. أما تغيير المتغيرات المنطقية فيعني البحث بين سلاسل useEffect.
| السيناريو | المتغيرات المنطقية | آلة الحالة |
|---|---|---|
| 2-3 حالات بسيطة | ✅ مقبول | ✅ أفضل |
| عمليات غير متزامنة | ❌ مليئة بالأخطاء | ✅ واضحة |
| تدفقات متعددة الخطوات | ❌ فوضوية | ✅ مثالية |
| أوضاع واجهة معقدة | ❌ مستحيلة | ✅ الخيار الأمثل |
إشارات المرور هي آلة الحالة النموذجية:
Red → Green → Yellow → Redتخيّل لو أن إشارات المرور استخدمت متغيرات منطقية:
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!مهندسو المرور يستخدمون آلات الحالة لأن الأرواح تعتمد على حالات يمكن التنبؤ بها.
وسلامة عقول مستخدميك تعتمد عليها أيضًا.
خذ هذا المكوّن الممتلئ بالمتغيرات المنطقية:
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
}حوّله إلى حالات صريحة:
states: browsing → cartOpen → checkoutForm → processing → complete
↘ error → checkoutForm (retry)نسخة آلة الحالة فيها 6 حالات صريحة بدل 32 تركيبة منطقية ممكنة.
آلات الحالة المنتهية ليست تمارين أكاديمية، بل أدوات عملية لإدارة التعقيد.
مكتبات مثل XState تجعل ذلك سهلًا في أي مشروع React/Vue/Svelte.