State Machines: XState

State Machine (конечный автомат) — математическая модель, описывающая систему через конечное множество состояний и переходы между ними; XState — JavaScript-библиотека для реализации state machines в приложениях.

Зачем нужно

Сложная логика с булевыми флагами (isLoading, isError, isSuccess, isRetrying) быстро становится неуправляемой: возможны невалидные комбинации (isLoading && isSuccess). State Machine явно описывает допустимые состояния и переходы — невалидные комбинации невозможны по определению. Это упрощает тестирование, устраняет баги типа «кнопка была кликнута дважды» и делает логику документируемой через диаграмму состояний.

Где используется

  • Многошаговые формы (wizard): каждый шаг — состояние, валидация — переход
  • Загрузка данных: idle → loading → success/error → retry
  • UI с комплексными состояниями: drag & drop, медиаплеер
  • Авторизационные флоу с несколькими шагами

Конечный автомат без библиотеки

// Простая state machine для загрузки данных
const fetchMachine = {
  initial: 'idle',
  states: {
    idle: {
      on: { FETCH: 'loading' }, // событие FETCH → переход в loading
    },
    loading: {
      on: {
        SUCCESS: 'success',
        ERROR: 'error',
      },
    },
    success: {
      on: { RESET: 'idle' },
    },
    error: {
      on: {
        RETRY: 'loading',
        RESET: 'idle',
      },
    },
  },
};

// Простой интерпретатор
function transition(machine, currentState, event) {
  const stateConfig = machine.states[currentState];
  const nextState = stateConfig?.on?.[event];
  return nextState || currentState; // если переход не описан — остаёмся
}

let state = fetchMachine.initial; // 'idle'
state = transition(fetchMachine, state, 'FETCH');    // 'loading'
state = transition(fetchMachine, state, 'SUCCESS');  // 'success'
state = transition(fetchMachine, state, 'FETCH');    // 'success' (нет такого перехода!)

XState: полноценная библиотека

import { createMachine, assign } from 'xstate';

const fetchMachine = createMachine({
  id: 'fetch',
  initial: 'idle',
  context: {
    data: null,
    error: null,
  },
  states: {
    idle: {
      on: { FETCH: 'loading' },
    },
    loading: {
      invoke: {
        // Автоматически вызывает промис при входе в состояние
        src: (context, event) => fetchData(event.url),
        onDone: {
          target: 'success',
          actions: assign({ data: (_, event) => event.data }),
        },
        onError: {
          target: 'error',
          actions: assign({ error: (_, event) => event.data }),
        },
      },
    },
    success: {
      on: { RESET: 'idle' },
    },
    error: {
      on: {
        RETRY: 'loading',
        RESET: 'idle',
      },
    },
  },
});

XState + React

import { useMachine } from '@xstate/react';

function DataFetcher({ url }) {
  const [state, send] = useMachine(fetchMachine);

  return (
    <div>
      {state.matches('idle') && (
        <button onClick={ => send({ type: 'FETCH', url })}>
          Загрузить
        </button>
      )}

      {state.matches('loading') && <p>Загрузка...</p>}

      {state.matches('success') && (
        <div>
          <pre>{JSON.stringify(state.context.data, null, 2)}</pre>
          <button onClick={ => send('RESET')}>Сбросить</button>
        </div>
      )}

      {state.matches('error') && (
        <div>
          <p>Ошибка: {state.context.error?.message}</p>
          <button onClick={ => send({ type: 'RETRY', url })}>Повторить</button>
        </div>
      )}
    </div>
  );
}

Частые ошибки

  • Используют XState для простых случаев — если состояния 2-3 и логика простая, хватит useState; XState добавляет бойлерплейт.
  • Не визуализируют машину — XState Visualizer позволяет увидеть граф состояний; не пренебрегайте этим при сложной логике.
  • Мутируют context напрямую — context в XState иммутабелен; изменения только через assign.

Связанные темы

Ресурсы