Создание редьюсера

Haunted — это современная библиотека для построения веб-компонентов с использованием реактивного состояния и хуков, вдохновленных React. Один из ключевых инструментов управления состоянием в Haunted — это редьюсер, позволяющий централизованно обновлять состояние компонента на основе действий (actions).


Паттерн редьюсера

Редьюсер в Haunted реализуется через хук useReducer, который имеет следующую сигнатуру:

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

Параметры:

  • reducer — функция, которая принимает текущее состояние и объект действия, возвращает новое состояние.
  • initialState — начальное состояние компонента.
  • state — текущее состояние, доступное внутри компонента.
  • dispatch — функция, с помощью которой отправляются действия для изменения состояния.

Структура функции редьюсера

Функция редьюсера имеет строго определённую форму:

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

Ключевые моменты:

  1. Иммутабельность состояния — новое состояние всегда создаётся через копирование старого (...state), а не через модификацию существующего объекта.
  2. Тип действия (action.type) — строковый идентификатор действия. Любое изменение состояния должно быть выражено через тип действия.
  3. Payload (дополнительные данные) — объект action может содержать дополнительные свойства (payload), используемые для более сложных обновлений.

Пример с payload:

function reducer(state, action) {
  switch (action.type) {
    case 'SET_NAME':
      return { ...state, name: action.payload };
    default:
      return state;
  }
}

Инициализация редьюсера в компоненте

Компонент Haunted с редьюсером создаётся следующим образом:

import { html, component, useReducer } from 'haunted';

function Counter() {
  const initialState = { count: 0 };

  function reducer(state, action) {
    switch (action.type) {
      case 'INCREMENT':
        return { ...state, count: state.count + 1 };
      case 'DECREMENT':
        return { ...state, count: state.count - 1 };
      default:
        return state;
    }
  }

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

  return html`
    <div>
      <p>Счётчик: ${state.count}</p>
      <button @click=${() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button @click=${() => dispatch({ type: 'DECREMENT' })}>-</button>
    </div>
  `;
}

customElements.define('counter-component', component(Counter));

Особенности реализации:

  • dispatch можно передавать напрямую в обработчики событий.
  • Каждое действие приводит к пересчёту состояния и повторному рендерингу компонента.
  • Состояние редьюсера полностью управляется функцией reducer, что упрощает тестирование и прогнозирование поведения.

Использование сложного состояния

Редьюсер удобно использовать для множественных свойств состояния или сложной логики обновления, когда простые хуки useState становятся неудобными.

Пример сложного состояния с вложенными объектами:

const initialState = {
  user: { name: '', age: 0 },
  settings: { darkMode: false }
};

function reducer(state, action) {
  switch (action.type) {
    case 'SET_NAME':
      return { ...state, user: { ...state.user, name: action.payload } };
    case 'TOGGLE_DARK_MODE':
      return { ...state, settings: { ...state.settings, darkMode: !state.settings.darkMode } };
    default:
      return state;
  }
}

Рекомендации при работе с вложенными объектами:

  • Не изменять вложенные объекты напрямую. Использовать операцию spread (...) для создания новых копий.
  • Стараться сохранять простую структуру действий: одно действие = одно изменение.

Комбинирование нескольких редьюсеров

Для больших компонентов часто имеет смысл разделять состояние на несколько редьюсеров и использовать их параллельно:

const [userState, userDispatch] = useReducer(userReducer, initialUserState);
const [settingsState, settingsDispatch] = useReducer(settingsReducer, initialSettingsState);

Такой подход повышает читаемость и позволяет логически разделять обработку действий.


Поддержка побочных эффектов

Хотя редьюсер сам по себе чистая функция и не должен содержать побочных эффектов, Haunted позволяет комбинировать редьюсер с useEffect для асинхронных операций:

useEffect(() => {
  if (state.count === 5) {
    console.log('Достигнут предел 5');
  }
}, [state.count]);

Принцип: редьюсер изменяет только состояние, а все побочные эффекты выполняются в useEffect, что делает архитектуру компонентов более предсказуемой.


Резюме принципов работы редьюсера

  • Состояние управляется централизованно функцией reducer.
  • Действия (action) описывают изменения состояния.
  • Иммутабельность и чистота функции редьюсера обеспечивают предсказуемость поведения.
  • Для сложных состояний или нескольких логических блоков удобно использовать несколько редьюсеров.
  • Побочные эффекты следует обрабатывать вне редьюсера, через useEffect.

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