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.
Связанные темы
- _MOC SPA
- State -- внутреннее состояние
- Локальное vs глобальное состояние
- Загрузка данных и loading states