Паттерны обновления состояния

Hyperapp строит работу со состоянием вокруг строгой иммутабельности. Состояние рассматривается как неизменяемая структура данных: любое обновление приводит к созданию нового объекта состояния, а не к модификации существующего. Это ключевое условие корректной работы диффинга и предсказуемого рендера.

Обновление состояния всегда происходит через actions, которые возвращают новое состояние целиком либо его часть. Даже если меняется одно поле, создаётся новый объект верхнего уровня:

const state = {
  count: 0
}

const actions = {
  increment: () => state => ({
    ...state,
    count: state.count + 1
  })
}

Такой подход:

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

Чистые функции обновления

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

Простейший паттерн — одно действие = одно изменение:

const actions = {
  setName: name => state => ({
    ...state,
    name
  })
}

Отсутствие логики вне action-функции гарантирует, что одинаковый вход всегда даёт одинаковый выход. Это облегчает тестирование и упрощает reasoning о поведении приложения.

Частичное обновление вложенного состояния

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

const state = {
  user: {
    profile: {
      email: ''
    }
  }
}

const actions = {
  updateEmail: email => state => ({
    ...state,
    user: {
      ...state.user,
      profile: {
        ...state.user.profile,
        email
      }
    }
  })
}

Данный паттерн гарантирует, что:

  • ссылки на неизменённые части состояния сохраняются;
  • виртуальный DOM может корректно определить, какие компоненты нуждаются в перерисовке.

Для глубоко вложенных структур рекомендуется нормализация состояния, а не усложнение копирования.

Декомпозиция состояния и действий

Крупное состояние логически делится на независимые сегменты. Каждому сегменту соответствует собственный набор actions. Hyperapp поддерживает композицию действий через вложенные объекты:

const state = {
  counter: 0,
  todos: []
}

const actions = {
  counter: {
    increment: () => state => state + 1
  },
  todos: {
    add: text => state => state.concat({ text, done: false })
  }
}

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

Использование эффектов без нарушения паттернов

Асинхронные операции и побочные эффекты выносятся в effects, которые не изменяют состояние напрямую. Effect возвращает действие, которое будет применено после завершения операции.

const actions = {
  loadData: () => [
    state => ({ ...state, loading: true }),
    fetchEffect
  ],
  setData: data => state => ({
    ...state,
    loading: false,
    data
  })
}

Здесь обновление состояния остаётся чистым, а побочные эффекты изолированы. Паттерн «action + effect» предотвращает смешение логики данных и ввода-вывода.

Последовательные обновления состояния

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

const actions = {
  submit: () => [
    state => ({ ...state, submitting: true }),
    sendFormEffect
  ]
}

Каждый шаг остаётся атомарным и предсказуемым. Такой паттерн предпочтительнее монолитных action-функций с большим количеством логики.

Производные данные и отказ от дублирования

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

Неправильно:

{
  items: [...],
  completedCount: 3
}

Правильно:

const completedCount = state.items.filter(i => i.done).length

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

Сброс и инициализация состояния

Для сброса состояния используется возвращение исходного объекта или его копии. Начальное состояние выносится в отдельную константу:

const initialState = {
  count: 0
}

const actions = {
  reset: () => () => initialState
}

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

Минимизация логики внутри actions

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

const calculateTotal = items =>
  items.reduce((sum, i) => sum + i.price, 0)

const actions = {
  updateItems: items => state => ({
    ...state,
    items,
    total: calculateTotal(items)
  })
}

Логика остаётся переиспользуемой, а action выполняет единственную ответственность — формирование нового состояния.

Предсказуемость как ключевой результат паттернов

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