Эффекты (Effects) и побочные действия

Hyperapp использует чистые функции для описания состояния и обновлений, что накладывает строгие ограничения на работу с побочными эффектами. Любая логика, изменяющая состояние вне непосредственного обновления состояния (например, асинхронные операции, работа с API, таймеры, локальное хранилище), должна оформляться через эффекты (effects). Это позволяет сохранять детерминированность и предсказуемость приложения.


Основы эффекта

Эффект в Hyperapp — это объект с ключами effect и dispatch. Простейший синтаксис:

const fetchUserEffect = (dispatch, props) => {
  fetch(`https://api.example.com/users/${props.id}`)
    .then(res => res.json())
    .then(data => dispatch({ type: "SET_USER", user: data }))
}

Использование эффекта в действии:

const actions = {
  loadUser: id => [
    { type: "NO_OP" }, // изменение состояния при необходимости
    fetchUserEffect,   // эффект будет выполнен
    id                 // параметры передаются в эффект
  ],
  setUser: user => state => ({ ...state, user })
}

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

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

Структура объекта эффекта

В Hyperapp 2 эффекты могут быть представлены как массив из двух элементов [effectFn, props]. Первый элемент — функция эффекта, второй — объект или значение, которое будет передано в функцию. Пример:

const logEffect = (dispatch, message) => {
  console.log(message)
}

const actions = {
  logMessage: message => [logEffect, message]
}

При вызове actions.logMessage("Hello World") эффект выполнится и выведет сообщение в консоль.


Асинхронные эффекты

Асинхронные операции полностью реализуются внутри эффекта, используя промисы или async/await. Например, загрузка данных с сервера:

const fetchTodos = async (dispatch) => {
  try {
    const response = await fetch("/api/todos")
    const todos = await response.json()
    dispatch({ type: "SET_TODOS", todos })
  } catch (error) {
    dispatch({ type: "SET_ERROR", error })
  }
}

const actions = {
  loadTodos: () => [fetchTodos]
}

Особенности асинхронных эффектов:

  • Все побочные действия изолированы от состояния.
  • Обработка ошибок выполняется внутри эффекта или через отдельное действие.
  • Асинхронный код не блокирует основной поток обновления состояния.

Комбинирование эффектов и действий

Hyperapp позволяет комбинировать несколько эффектов в одном действии. Для этого действия могут возвращать массив эффектов и действий:

const actions = {
  init: () => [
    { type: "INIT_STATE" },
    fetchTodos,
    logEffect, "Инициализация завершена"
  ]
}

Порядок выполнения:

  1. Применение изменений состояния из синхронных действий.
  2. Выполнение всех эффектов с передачей dispatch.
  3. Любые последующие действия могут быть вызваны через dispatch внутри эффектов.

Очистка эффектов

Для эффектов, создающих подписки или таймеры, важно возвращать функцию очистки (cleanup) для предотвращения утечек памяти:

const timerEffect = (dispatch) => {
  const id = setInterval(() => dispatch({ type: "TICK" }), 1000)
  return () => clearInterval(id) // cleanup
}

const actions = {
  startTimer: () => [timerEffect]
}

Hyperapp автоматически вызывает возвращённую функцию при обновлении состояния, если эффект был перезаписан другим действием.


Эффекты и чистота функций

  • Действия (actions) остаются чистыми, они только формируют новые состояния и массивы эффектов.
  • Эффекты (effects) — единственное место для работы с внешним миром: HTTP, локальное хранилище, таймеры, подписки на события.
  • Такая структура гарантирует предсказуемость и облегчает тестирование: действия можно тестировать без выполнения побочных эффектов.

Примеры практического применения

  1. Запрос к API при загрузке страницы
const fetchPosts = async (dispatch) => {
  const res = await fetch("/api/posts")
  const posts = await res.json()
  dispatch({ type: "SET_POSTS", posts })
}

const actions = {
  loadPosts: () => [fetchPosts]
}
  1. Подписка на события окна
const resizeEffect = (dispatch) => {
  const handler = () => dispatch({ type: "WINDOW_RESIZE", size: window.innerWidth })
  window.addEventListener("resize", handler)
  return () => window.removeEventListener("resize", handler)
}

const actions = {
  subscribeResize: () => [resizeEffect]
}
  1. Локальное хранилище
const saveToLocalStorage = (dispatch, state) => {
  localStorage.setItem("appState", JSON.stringify(state))
}

const actions = {
  saveState: () => [saveToLocalStorage]
}

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