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 не предоставляет специальных абстракций для модулей, но его минимализм делает композицию естественным следствием правильной архитектуры. Всё приложение остаётся набором чистых функций, а сложность управляется структурой, а не дополнительными слоями.