Концепция редьюсера

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


Определение редьюсера

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

Сигнатура редьюсера выглядит так:

function reducer(state, action) {
  // Возвращает новое состояние
}
  • state — текущие данные состояния компонента.
  • action — объект, описывающий событие или команду для изменения состояния. Обычно имеет структуру { type: 'ACTION_TYPE', payload: any }.

Пример базового редьюсера:

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

Использование редьюсера с useReducer

Haunted предоставляет хук useReducer, который интегрирует редьюсер в функциональный компонент:

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

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

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

customElements.define('x-counter', component(Counter));
  • useReducer(reducer, initialState) возвращает массив [state, dispatch]:

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

Ключевой момент: каждое изменение состояния через dispatch вызывает повторный рендер компонента с новым состоянием.


Структура действий (actions)

Actions — это объекты, которые описывают какое изменение должно произойти.

Пример расширенного использования:

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { text: action.payload, completed: false }];
    case 'TOGGLE_TODO':
      return state.map((todo, idx) =>
        idx === action.payload ? { ...todo, completed: !todo.completed } : todo
      );
    case 'REMOVE_TODO':
      return state.filter((_, idx) => idx !== action.payload);
    default:
      return state;
  }
}
  • payload содержит данные, необходимые для выполнения действия.
  • Редьюсер всегда возвращает новую структуру состояния, сохраняя иммутабельность.

Особенности редьюсера в Haunted

  1. Чистота функции: редьюсер не должен вызывать побочные эффекты (например, сетевые запросы или манипуляции с DOM).
  2. Предсказуемость: любое действие с одинаковым состоянием всегда возвращает одинаковый результат.
  3. Иммутабельность: старое состояние никогда не изменяется напрямую; всегда создается новый объект или массив.
  4. Скалируемость: редьюсеры позволяют организовать сложное состояние в несколько подредьюсеров, объединяя их по принципу combineReducers (аналогично Redux).

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

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

function todosReducer(state, action) { /* как выше */ }
function filterReducer(state, action) {
  switch (action.type) {
    case 'SET_FILTER':
      return action.payload;
    default:
      return state;
  }
}

function rootReducer(state, action) {
  return {
    todos: todosReducer(state.todos, action),
    filter: filterReducer(state.filter, action)
  };
}

const initialState = { todos: [], filter: 'all' };

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


Применение редьюсеров в реальных компонентах

Редьюсеры особенно полезны, когда:

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

Пример компонента с несколькими редьюсерами:

function App() {
  const [state, dispatch] = useReducer(rootReducer, initialState);

  return html`
    <div>
      <x-todo-list
        .todos=${state.todos}
        .filter=${state.filter}
        @add=${e => dispatch({ type: 'ADD_TODO', payload: e.detail })}
        @toggle=${e => dispatch({ type: 'TOGGLE_TODO', payload: e.detail })}
      ></x-todo-list>

      <x-filter
        .currentFilter=${state.filter}
        @change=${e => dispatch({ type: 'SET_FILTER', payload: e.detail })}
      ></x-filter>
    </div>
  `;
}

Рекомендации по организации редьюсеров

  • Действия должны иметь чётко определённый тип (type) и содержать минимально необходимые данные (payload).
  • Состояние лучше структурировать в виде объектов и массивов, избегая примитивов в качестве корневого состояния.
  • Для крупных приложений стоит разделять редьюсеры на модули, чтобы каждая часть состояния управлялась отдельной функцией.
  • Тестирование редьюсеров просто: достаточно передавать различные комбинации состояния и действий и проверять возвращаемое значение.

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