Hyperapp строится вокруг идеи чистых функций и неизменяемого состояния. Компонент в этом контексте — не класс и не объект, а функция, возвращающая виртуальное дерево представления. Компоненты не хранят собственное состояние в привычном смысле; они получают данные и действия извне и описывают, как из них формируется интерфейс.
Минимальный компонент представляет собой функцию, возвращающую vnode:
const Title = (props) =>
<h1>{props.text}</h1>
Здесь компонент не содержит побочных эффектов и не зависит от внешнего окружения. Такой подход позволяет свободно комбинировать компоненты и гарантирует предсказуемость рендеринга.
Композиция компонентов в Hyperapp основана на передаче свойств (props). Любые данные, необходимые для отображения, передаются в виде аргумента функции-компонента.
const UserCard = ({ name, age }) =>
<div class="user-card">
<span class="name">{name}</span>
<span class="age">{age}</span>
</div>
Использование компонента внутри другого компонента выглядит как обычное вложение:
const App = () =>
<main>
<UserCard name="Ирина" age={29} />
<UserCard name="Алексей" age={34} />
</main>
Компоненты остаются полностью детерминированными: одинаковые входные данные всегда дают одинаковый результат.
Состояние приложения в Hyperapp хранится централизованно. Компоненты
получают доступ к нему через аргументы state и
actions, передаваемые корневому представлению.
const state = {
count: 0
}
const actions = {
increment: () => state => ({ count: state.count + 1 })
}
Компонент, зависящий от состояния, описывается как функция от
state и actions:
const Counter = (state, actions) =>
<div>
<span>{state.count}</span>
<button oncl ick={actions.increment}>+</button>
</div>
Такой компонент не владеет состоянием, а лишь отображает его часть и связывает действия с событиями.
Крупные представления удобно разбивать на подкомпоненты, которым передаётся только необходимый фрагмент состояния и соответствующие действия.
const CounterValue = ({ value }) =>
<span class="value">{value}</span>
const CounterButton = ({ onClick }) =>
<button oncl ick={onClick}>+</button>
const Counter = (state, actions) =>
<div>
<CounterValue value={state.count} />
<CounterButton onCl ick={actions.increment} />
</div>
Декомпозиция снижает связанность и делает компоненты повторно используемыми. Каждый подкомпонент можно тестировать изолированно, подставляя фиктивные данные.
Hyperapp позволяет строить компоненты высшего порядка — функции, которые возвращают другие компоненты. Такой приём используется для обобщения логики и структурных шаблонов.
const withBorder = (Component) => (props) =>
<div class="border">
<Component {...props} />
</div>
Применение обёртки:
const BorderedTitle = withBorder(Title)
const App = () =>
<BorderedTitle text="Заголовок" />
Композиция через функции не требует специальных API и остаётся частью обычного JavaScript.
Условное отображение элементов реализуется стандартными выражениями языка. Компоненты могут возвращать разные поддеревья в зависимости от входных данных.
const Status = ({ online }) =>
online
? <span class="online">Онлайн</span>
: <span class="offline">Оффлайн</span>
Для более сложных случаев используется логическое И:
const Notification = ({ message }) =>
<div>
{message && <p>{message}</p>}
</div>
Такой стиль не вводит дополнительных абстракций и остаётся прозрачным.
Рендеринг списков строится на стандартном методе map,
возвращающем массив vnode-элементов.
const TodoItem = ({ text, done }) =>
<li class={done ? "done" : ""}>{text}</li>
const TodoList = ({ items }) =>
<ul>
{items.map(item =>
<TodoItem text={item.text} done={item.done} />
)}
</ul>
Каждый элемент списка является полноценным компонентом, что упрощает расширение логики и изменение внешнего вида.
Компоненты в Hyperapp рекомендуется держать максимально простыми. Вся бизнес-логика выносится в actions или отдельные функции, не связанные с представлением.
const formatName = (user) =>
`${user.lastName} ${user.firstName}`
const UserName = ({ user }) =>
<span>{formatName(user)}</span>
Такое разделение облегчает повторное использование кода и поддерживает функциональный стиль, на котором основан фреймворк.
Композиция компонентов формирует явную иерархию. Верхние уровни управляют состоянием и передают данные вниз, нижние уровни отвечают исключительно за отображение.
Отсутствие двусторонних связей между компонентами исключает скрытые зависимости. Любое изменение структуры интерфейса выражается через перестройку дерева функций, а не через неявные механизмы жизненного цикла.
Компоненты проектируются так, чтобы их поведение определялось параметрами, а не жёстко зашитой логикой.
const Button = ({ label, onClick, type }) =>
<button class={type} oncl ick={onClick}>
{label}
</button>
Один и тот же компонент может использоваться в разных контекстах, получая различные свойства и действия, оставаясь при этом неизменным на уровне реализации.