Вложенные состояния и структуры данных

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


Состояние в Hyperapp — это обычный JavaScript-объект, передаваемый в функцию app. Он может содержать любые допустимые структуры: примитивы, массивы, объекты, вложенные комбинации.

const state = {
  user: {
    id: 1,
    name: "Alex",
    settings: {
      theme: "dark",
      notifications: true
    }
  },
  todos: [
    { id: 1, title: "Learn Hyperapp", done: false }
  ]
}

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


Иммутабельность и вложенные обновления

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

Некорректный подход:

state.user.settings.theme = "light"
return state

Корректный подход:

(state) => ({
  ...state,
  user: {
    ...state.user,
    settings: {
      ...state.user.settings,
      theme: "light"
    }
  }
})

Каждый уровень копируется с помощью оператора расширения (...), что позволяет Hyperapp корректно определить изменение состояния и перерисовать интерфейс.


Действия и вложенные структуры

Действия (actions) в Hyperapp могут быть организованы зеркально структуре состояния. Это повышает читаемость и логическую целостность кода.

const actions = {
  user: {
    setName: (name) => (state) => ({
      ...state,
      name
    }),
    settings: {
      toggleNotifications: () => (state) => ({
        ...state,
        notifications: !state.notifications
      })
    }
  }
}

При такой структуре state внутри действия соответствует текущему поддереву состояния, а не всему корневому объекту. Это принципиально важно: вложенные действия получают доступ только к своей части данных.


Локальность состояния и композиция

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

const Settings = ({ state, actions }) => (
  <div>
    <span>{state.theme}</span>
    <button oncl ick={actions.toggleTheme}>Toggle</button>
  </div>
)

Передача осуществляется на уровне представления:

Settings({
  state: state.user.settings,
  actions: actions.user.settings
})

Такой подход формирует чёткие границы ответственности и упрощает повторное использование компонентов.


Работа с массивами во вложенном состоянии

Массивы во вложенных структурах требуют особой аккуратности. Любое изменение элемента массива также должно быть иммутабельным.

Пример обновления элемента списка:

const toggleTodo = (id) => (state) => ({
  ...state,
  todos: state.todos.map(todo =>
    todo.id === id
      ? { ...todo, done: !todo.done }
      : todo
  )
})

Здесь создаётся новый массив и новый объект только для изменяемого элемента, что минимизирует накладные расходы и сохраняет корректность.


Нормализация данных

При росте вложенности структуры становятся сложными для поддержки. Часто применяется нормализация — разбиение данных на словари по идентификаторам.

Вместо:

projects: [
  {
    id: 1,
    tasks: [
      { id: 10, title: "Task A" }
    ]
  }
]

Используется:

projects: {
  byId: {
    1: { id: 1, taskIds: [10] }
  }
},
tasks: {
  byId: {
    10: { id: 10, title: "Task A" }
  }
}

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


Вычисляемые подструктуры

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

const getCompletedTodos = (state) =>
  state.todos.filter(t => t.done)

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


Частичные обновления и композиция действий

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

const actions = {
  counter: {
    increment: () => (state) => ({ value: state.value + 1 })
  }
}

Здесь возвращается только обновлённое поддерево counter, без необходимости вручную копировать корневой объект.


Ошибки проектирования вложенных состояний

Частые проблемы при работе со вложенными структурами:

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

Эти ошибки приводят к трудноуловимым багам и непредсказуемому поведению интерфейса.


Принципы проектирования

Эффективная работа с вложенными состояниями в Hyperapp опирается на несколько принципов:

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

Такая архитектура сохраняет минимализм Hyperapp, позволяя при этом строить масштабируемые и поддерживаемые приложения.