State Machines

State machine (машина состояний) — это модель управления состояниями, которая позволяет описывать поведение компонента или приложения через конечное количество состояний и переходов между ними. В Preact использование state machine упрощает управление сложной логикой компонентов, особенно когда состояние зависит от множества факторов и событий.


Основные концепции

Состояние (State) — это текущее положение компонента в его жизненном цикле. В традиционном React/Preact это объект или примитив, задаваемый через useState или useReducer.

Событие (Event) — действие или триггер, который инициирует переход из одного состояния в другое. Событие может быть вызвано пользовательским взаимодействием, таймером или изменением данных.

Переход (Transition) — определяет, как одно состояние изменяется в другое под воздействием события. Переходы часто описываются в виде таблицы или объекта, где ключ — текущее состояние и событие, а значение — новое состояние.

Стартовое состояние (Initial State) — состояние, в котором компонент находится при инициализации. В Preact оно обычно задается в хуке useState или useReducer.


Применение State Machines в Preact

Для интеграции машины состояний с Preact можно использовать хуки и чистые функции. Рассмотрим подход с useReducer, который идеально подходит для реализации state machine:

import { h } from 'preact';
import { useReducer } from 'preact/hooks';

const initialState = { status: 'idle' };

function reducer(state, action) {
  switch (state.status) {
    case 'idle':
      if (action.type === 'FETCH') return { status: 'loading' };
      return state;
    case 'loading':
      if (action.type === 'SUCCESS') return { status: 'success', data: action.data };
      if (action.type === 'ERROR') return { status: 'error', error: action.error };
      return state;
    case 'success':
    case 'error':
      if (action.type === 'RESET') return { status: 'idle' };
      return state;
    default:
      return state;
  }
}

function FetchComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const fetchData = async () => {
    dispatch({ type: 'FETCH' });
    try {
      const response = await fetch('/api/data');
      const data = await response.json();
      dispatch({ type: 'SUCCESS', data });
    } catch (error) {
      dispatch({ type: 'ERROR', error });
    }
  };

  return (
    <div>
      {state.status === 'idle' && <button onCl ick={fetchData}>Загрузить данные</button>}
      {state.status === 'loading' && <p>Загрузка...</p>}
      {state.status === 'success' && <pre>{JSON.stringify(state.data, null, 2)}</pre>}
      {state.status === 'error' && <p>Ошибка: {state.error.message}</p>}
    </div>
  );
}

Ключевые моменты данного подхода:

  • useReducer позволяет централизованно управлять всеми состояниями и переходами.
  • Каждое событие описано явно, что уменьшает вероятность неожиданных переходов.
  • Легко добавлять новые состояния и события без изменения всей структуры компонента.

Табличное представление переходов

Для сложных компонентов удобно описывать переходы в виде объекта:

const transitions = {
  idle: { FETCH: 'loading' },
  loading: { SUCCESS: 'success', ERROR: 'error' },
  success: { RESET: 'idle' },
  error: { RESET: 'idle' },
};

function reducer(state, action) {
  const nextState = transitions[state.status]?.[action.type];
  return nextState ? { ...state, status: nextState, ...action.payload } : state;
}

Такой подход делает логику декларативной, упрощает отладку и документирование.


Взаимодействие с Preact Hooks

State machine хорошо интегрируется с хуками жизненного цикла:

  • useEffect — для запуска побочных эффектов при переходе в определённое состояние:
useEffect(() => {
  if (state.status === 'loading') {
    const timer = setTimeout(() => dispatch({ type: 'ERROR', error: new Error('Timeout') }), 5000);
    return () => clearTimeout(timer);
  }
}, [state.status]);
  • useMemo / useCallback — для оптимизации вычислений и обработки событий, чтобы не пересоздавать функции при каждом рендере.

Преимущества использования state machine в Preact

  1. Предсказуемость поведения — все состояния и переходы описаны явно.
  2. Упрощение тестирования — легче покрывать тестами отдельные состояния и события.
  3. Поддержка сложной логики UI — удобно описывать многоуровневые состояния с множественными ответвлениями.
  4. Декларативность — легко визуализировать и документировать логику через диаграммы состояний.

Расширение: композиция машин состояний

В крупных приложениях часто возникает необходимость комбинировать несколько машин состояний. В Preact это реализуется через вложенные редьюсеры или отдельные компоненты, управляющие своей локальной машиной:

function ParentComponent() {
  const [parentState, parentDispatch] = useReducer(parentReducer, parentInitialState);

  return (
    <div>
      {parentState.status === 'active' && <ChildComponent />}
      <button onCl ick={() => parentDispatch({ type: 'TOGGLE' })}>Переключить</button>
    </div>
  );
}

Каждый дочерний компонент может иметь собственную машину состояний, при этом события родителя могут инициировать переходы в дочерних машинах через пропсы или контекст.


Резюме по использованию

  • State machine делает код чётким, предсказуемым и расширяемым.
  • Preact с хуками (useReducer, useEffect) идеально подходит для реализации таких моделей.
  • Табличное и декларативное описание переходов улучшает читаемость и поддержку кода.
  • Комбинирование нескольких машин позволяет строить сложные интерфейсы без хаоса в состояниях.

State machine становится мощным инструментом для управления сложной логикой в Preact-приложениях, превращая хаотичное состояние в структурированную, легко управляемую систему.