Development 7 دقيقة للقراءة 1,495 مشاهدات

آلات الحالة في تطوير الواجهات: دليل عملي لـ XState 5

متى تتفوق آلة الحالة على useState، وكيف تبنيها بـ XState 5 — صياغة الفاعلين، وآلة جلب بيانات حقيقية، والربط مع React، والاختبار، ومتى تتجنبها.

آلات الحالة في تطوير الواجهات: دليل عملي لـ XState 5

معظم أخطاء الواجهات التي توصف بأنها «مستحيلة» هي الخطأ نفسه: قيمتان منطقيتان ما كان ينبغي أن تصحّا معًا. isLoading وisError مفعّلتان معًا. أو نموذج في حالة submitting وeditable في آن واحد. أو نافذة مغلقة لكنها ما تزال تحمل بيانات.

آلة الحالة تزيل هذا الصنف من الأخطاء بحكم البناء لا بحكم الحذر، إذ تجعل التركيبات غير الصالحة غير قابلة للتمثيل أصلًا. ويغطي هذا الدليل متى يستحق ذلك، وكيف يبدو في XState 5.32 مع @xstate/react 6.1.

مشكلة الحالة المنطقية

const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [data, setData] = useState(null)

أربع قيم منطقية تعني ستة عشر تركيبًا ممكنًا، وربما أربعة منها صالحة. أما الاثنا عشر الباقية فأخطاء تنتظر ظرف تسابق مناسبًا: مستخدم يضغط «إعادة» وطلب قيد التنفيذ، أو استجابة تصل بعد أن انتقل المكوّن.

ولا يمكنك الخروج من هذا بالاختبارات وحدها، لأن الحالات غير الصالحة قابلة للوصول بحكم التعريف. والحل أن تجعلها غير قابلة للوصول.

الشيء نفسه كآلة حالة

import { setup, assign, fromPromise } from 'xstate'

export const dataMachine = setup({
  types: {
    context: {} as { data: User[] | null; error: string | null },
    events: {} as { type: 'FETCH' } | { type: 'RETRY' } | { type: 'CANCEL' },
  },
  actors: {
    fetchUsers: fromPromise(async () => {
      const res = await fetch('/api/users')
      if (!res.ok) throw new Error('Request failed')
      return res.json()
    }),
  },
}).createMachine({
  id: 'data',
  initial: 'idle',
  context: { data: null, error: null },
  states: {
    idle: { on: { FETCH: 'loading' } },
    loading: {
      invoke: {
        src: 'fetchUsers',
        onDone: {
          target: 'success',
          actions: assign({ data: ({ event }) => event.output, error: null }),
        },
        onError: {
          target: 'failure',
          actions: assign({ error: ({ event }) => String(event.error) }),
        },
      },
      on: { CANCEL: 'idle' },
    },
    success: { on: { FETCH: 'loading' } },
    failure: { on: { RETRY: 'loading' } },
  },
})

أربع حالات، لا أكثر. ولا سبيل لأن تكون «قيد التحميل» و«فاشلة» معًا، لأن الآلة تكون في حالة واحدة بالضبط في كل لحظة. والتركيبات الاثنا عشر غير الصالحة غير موجودة أصلًا كي تُختبر.

ولاحظ أمرًا آخر: RETRY مقبول في حالة failure فقط. أرسله أثناء التحميل فلا يحدث شيء — دون شرط حارس ولا خروج مبكر، فالآلة ببساطة لا تملك انتقالًا له.

ما الذي تغيّر في XState 5

XState 4XState 5
الإعدادcreateMachine مع كائن خياراتsetup({...}).createMachine({...})
الأنواععبر Generics، غالبًا مرهقةتُعلَن في setup.types
الخدماتservicesactors
وسائط الإجراءات(context, event)({ context, event })
الوعودinvoke مع وعدfromPromise()

والنتيجة العملية أن استنتاج TypeScript صار أفضل بكثير. فإعلان أحداثك في setup يمنحك تحققًا شاملًا: أرسل حدثًا لا تتعامل معه الآلة فيصبح خطأ تصريف لا تجاهلًا صامتًا.

استخدامها في React

import { useMachine } from '@xstate/react'
import { dataMachine } from './dataMachine'

export function UserList() {
  const [state, send] = useMachine(dataMachine)

  if (state.matches('idle')) {
    return <button onClick={() => send({ type: 'FETCH' })}>Load users</button>
  }

  if (state.matches('loading')) {
    return <Spinner />
  }

  if (state.matches('failure')) {
    return (
      <div role="alert">
        <p>{state.context.error}</p>
        <button onClick={() => send({ type: 'RETRY' })}>Try again</button>
      </div>
    )
  }

  return <ul>{state.context.data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}

صار المكوّن دالة عرض لقيمة واحدة. فلا تركيبات أعلام تحتاج التفكير فيها، وإضافة حالة لاحقًا تعني إضافة فرع سيشير إليه المصرّف.

الحرّاس والسياق

guards: {
  hasItems: ({ context }) => context.items.length > 0,
  underRetryLimit: ({ context }) => context.attempts < 3,
},
// ...
paying: {
  on: {
    RETRY: [
      { target: 'paying', guard: 'underRetryLimit',
        actions: assign({ attempts: ({ context }) => context.attempts + 1 }) },
      { target: 'failed' },
    ],
  },
},

ومصفوفة الإعادة تستحق قراءة متأنية: تُقيَّم الانتقالات بالترتيب، ويفوز أول انتقال يجتاز حارسه. فدون ثلاث محاولات يعيد، وإلا سقط إلى failed. وهكذا يصبح حد الإعادة جزءًا من تعريف الآلة لا شرطًا مدفونًا في معالج.

الاختبار

import { createActor } from 'xstate'

it('يتجاهل RETRY أثناء التحميل', () => {
  const actor = createActor(dataMachine).start()

  actor.send({ type: 'FETCH' })
  expect(actor.getSnapshot().value).toBe('loading')

  actor.send({ type: 'RETRY' })
  expect(actor.getSnapshot().value).toBe('loading') // بلا تغيير
})

وcan() هي الأداة المفيدة هنا، إذ تجيب عن سؤال «هل هذا الحدث صالح الآن؟» دون آثار جانبية — وهو مفيد في الاختبارات وفي تعطيل الأزرار من مصدر الحقيقة نفسه.

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

  • المفاتيح الثنائية. قائمة منسدلة مفتوحة أو مغلقة تكفيها useState، وتغليفها بآلة مجرد مراسم.
  • حالة الخادم. التخزين المؤقت وإعادة التحقق وإزالة التكرار هي ما وُجدت TanStack Query لأجله، فلا تُعد بناءها كآلة.
  • النماذج البسيطة. يغطي React Hook Form التحقق والإرسال جيدًا. واستخدم آلة حين يكون النموذج تدفقًا متعدد الخطوات بتفرعات.
  • حين لن يصونها الفريق. فآلة لا يفهمها غيرك أسوأ من أعلام يفهمها الجميع. جرّبها في تدفق معقد واحد وراقب كيف يجدها الفريق.

والاختبار الصادق: عُدّ التركيبات غير الصالحة التي تسمح بها حالتك الحالية. إن كانت صفرًا أو واحدة فلا تحتاج آلة، وإن كانت اثنتي عشرة فأنت تملك آلة بالفعل — لكنها ضمنية وموزّعة عبر المعالجات.

أين تستحق الآلات كلفتها

الدفع متعدد الخطوات والتهيئة الأولية. ومشغّلات الوسائط بما فيها التخزين المؤقت والتقديم واستعادة الأخطاء. وكل ما فيه إعادة محاولة وإلغاء. والسحب والإفلات. وتدفقات المصادقة بتحديات العامل الثاني ومسارات الاسترداد. والعمليات الطويلة ذات التقدم والتراجع.

والنمط واحد في كلها: أوضاع متمايزة فعلًا، وانتقالات صالحة من أوضاع بعينها فقط، وكلفة حقيقية حين يصل تركيب غير صالح إلى المستخدم.

أسئلة شائعة

متى أستخدم آلة حالة بدل useState؟

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

ما الذي تغيّر بين XState 4 و XState 5؟

تُعرَّف الآلات الآن عبر setup({...}).createMachine({...})، وتُعلَن الأنواع في setup.types بدل Generics، وصارت services تُسمى actors، وتستخدم الوعود fromPromise()، ووسائط الإجراءات كائن واحد. ونتج عن ذلك استنتاج أقوى بكثير في TypeScript.

هل XState مبالغة لتطبيق صغير؟

غالبًا نعم. فهي تستحق كلفتها في التدفقات ذات الأوضاع المتعددة والانتقالات غير الصالحة: الدفع والتهيئة وتشغيل الوسائط والإعادة والإلغاء. أما المفاتيح والنماذج البسيطة فهي مراسم بلا فائدة.

هل تحل XState محل Redux أو Zustand؟

لا، فهي تحل مشكلة مختلفة. إذ يحمل Redux و Zustand حالة التطبيق المشتركة، بينما تصف XState كيف ينتقل شيء واحد بين أوضاعه. وكثير من التطبيقات يستخدم الاثنين: مخزنًا للبيانات العامة وآلات للتدفقات المعقدة.

هل أستخدم XState لجلب البيانات؟

عادةً لا وحدها، فـ TanStack Query تتولى التخزين المؤقت وإعادة التحقق وإزالة التكرار. واستخدم آلة حين يكون الجلب جزءًا من تدفق أكبر فيه إلغاء أو حدود إعادة أو تفرّع بعده.

كيف أختبر آلة XState؟

أنشئ فاعلًا بـ createActor()، وأرسل الأحداث، وتحقّق من getSnapshot().value ومن السياق. ولا حاجة إلى عرض أي شيء، فتكون الاختبارات سريعة. وتتيح can() التأكد من رفض حدث غير صالح فعلًا.

كيف أستخدم XState مع React؟

ثبّت @xstate/react واستدعِ useMachine(machine)، فيعيد الحالة الحالية ودالة إرسال. وفرّع العرض على state.matches(...) واقرأ البيانات من state.context.

هل آلات الحالة والمخططات الحالية شيء واحد؟

المخطط الحالي هو آلة حالة منتهية مضافًا إليها التسلسل الهرمي والحالات المتوازية والتاريخ. وتنفّذ XState المخططات الحالية، فتستطيع البدء بحالات مسطّحة وإضافة التداخل فقط حين يحتاجه التدفق فعلًا.

مشاركة هذه المقالة:
ES
كتبه

Edrees Salih

مهندس برمجيات متكامل يتمتع بخبرة 9 سنوات. شغوف ببناء حلول قابلة للتطوير ومشاركة المعرفة مع مجتمع المطورين.

عرض الملف الشخصي

التعليقات (0)

اترك تعليقًا

لن يتم نشر بريدك الإلكتروني.

لا توجد تعليقات بعد. كن أول من يشارك أفكاره!

مقالات ذات صلة

مقالات ذات صلة

هل تحتاج مساعدة في مشروعك؟

احجز استشارة مجانية لمدة 30 دقيقة لمناقشة تحدياتك التقنية واستكشاف الحلول معًا.