Композиция состояний в больших приложениях

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

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


Структурирование состояния как дерева доменов

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

const state = {
  auth: {
    user: null,
    token: null,
    loading: false
  },
  todos: {
    items: [],
    filter: "all"
  },
  ui: {
    modalOpen: false
  }
}

Такое дерево облегчает:

  • локализацию изменений;
  • повторное использование логики;
  • тестирование отдельных частей.

Каждое поддерево может рассматриваться как мини-приложение со своим состоянием и набором действий.


Локальные действия и их композиция

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

const authActions = {
  login: credentials => state => ({
    ...state,
    loading: true
  }),

  loginSuccess: user => state => ({
    user,
    token: user.token,
    loading: false
  })
}

При композиции действий важно сохранять соответствие формы состояния и формы действий:

const actions = {
  auth: authActions,
  todos: todosActions,
  ui: uiActions
}

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


Изоляция логики обновления состояния

Одно из преимуществ Hyperapp — чистые функции обновления состояния. При композиции это свойство особенно важно: каждая часть состояния обновляется только своими действиями.

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

const todosActions = {
  add: text => state => ({
    ...state,
    items: state.items.concat({
      id: Date.now(),
      text,
      completed: false
    })
  })
}

Здесь state — это todos, а не всё приложение. Отсутствие знания о глобальной структуре делает действия устойчивыми к рефакторингу верхнего уровня.


Взаимодействие между поддеревьями состояния

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

const actions = {
  auth: authActions,
  todos: todosActions,

  logout: () => (state, actions) => {
    actions.auth.clear()
    actions.todos.reset()
  }
}

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

  • сохраняет изоляцию доменов;
  • явно описывает точки взаимодействия;
  • предотвращает скрытые зависимости.

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

Hyperapp поддерживает эффекты как возвращаемые значения из действий. При композиции состояний важно не смешивать эффекты разных доменов в одном месте без необходимости.

Пример доменного эффекта:

const fetchTodos = userId => [
  state => ({ ...state, loading: true }),
  [api.getTodos, { userId }]
]

На уровне композиции эффект может быть обёрнут или переиспользован, но ответственность за его форму остаётся внутри домена.


Связывание состояния с представлением

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

const TodosView = (state, actions) =>
  h("ul", {},
    state.items.map(item =>
      h("li", {}, item.text)
    )
  )

На верхнем уровне:

const view = (state, actions) =>
  h("main", {}, [
    AuthView(state.auth, actions.auth),
    TodosView(state.todos, actions.todos)
  ])

Такое разделение:

  • снижает связность компонентов;
  • упрощает повторное использование;
  • делает структуру приложения прозрачной.

Повторное использование поддеревьев состояния

Композиция позволяет выделять обобщённые поддеревья, пригодные для повторного использования. Например, стандартное состояние загрузки:

const createAsyncState = () => ({
  loading: false,
  error: null
})

И его встраивание:

const state = {
  todos: {
    ...createAsyncState(),
    items: []
  }
}

Аналогично выносятся фабрики действий, что особенно полезно при создании однотипных модулей.


Масштабирование и поддерживаемость

Грамотно скомпонованное состояние:

  • уменьшает когнитивную нагрузку;
  • упрощает навигацию по коду;
  • позволяет команде работать параллельно над разными доменами.

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