Компоненты высшего порядка (Higher-Order Components, HOC) представляют собой паттерн проектирования, позволяющий расширять функциональность существующих компонентов без модификации их внутреннего кода. В контексте Mithril HOC реализуются через функции, принимающие компонент и возвращающие новый компонент с дополнительным поведением или состоянием.
HOC позволяет декорировать компонент, добавляя к нему новые свойства, обработчики событий или логику жизненного цикла. В отличие от наследования, HOC использует композицию, что делает код более модульным и переиспользуемым.
Простейший пример HOC:
function withLogger(component) {
return {
oninit(vnode) {
console.log('Компонент инициализирован', vnode.attrs);
if (component.oninit) component.oninit(vnode);
},
view(vnode) {
return component.view(vnode);
}
};
}
const Button = {
view() {
return m('button', 'Нажми меня');
}
};
const ButtonWithLogger = withLogger(Button);
m.mount(document.body, ButtonWithLogger);
В этом примере withLogger расширяет любой компонент
логированием его инициализации.
HOC должен правильно передавать атрибуты и children
в исходный компонент, чтобы не нарушать его функциональность. Для
управления состоянием HOC может использовать собственные поля в
vnode.state или оборачивать исходный компонент в
контролируемый контейнер:
function withToggle(component) {
return {
oninit(vnode) {
vnode.state.open = false;
vnode.state.toggle = () => {
vnode.state.open = !vnode.state.open;
};
if (component.oninit) component.oninit(vnode);
},
view(vnode) {
return component.view({
...vnode,
attrs: { ...vnode.attrs, open: vnode.state.open, toggle: vnode.state.toggle }
});
}
};
}
const Dropdown = {
view(vnode) {
return m('div', [
m('button', { onclick: vnode.attrs.toggle }, 'Меню'),
vnode.attrs.open && m('ul', [
m('li', 'Пункт 1'),
m('li', 'Пункт 2')
])
]);
}
};
const DropdownWithToggle = withToggle(Dropdown);
m.mount(document.body, DropdownWithToggle);
В данном случае HOC добавляет локальное состояние и функцию переключения, которую использует исходный компонент.
Можно создавать цепочки HOC, оборачивая компонент несколько раз:
const EnhancedDropdown = withLogger(withToggle(Dropdown));
m.mount(document.body, EnhancedDropdown);
При этом важно соблюдать порядок вызовов, так как первый HOC оборачивает исходный компонент, а последующие HOC получают уже изменённый компонент.
HOC может внедрять логику во все стадии жизненного цикла Mithril:
oninit, oncreate, onupdate,
onbeforeremove и onremove. При этом нужно
аккуратно вызывать методы исходного компонента, чтобы не
нарушить его собственные жизненные циклы:
function withLifecycleLog(component) {
return {
oninit(vnode) {
console.log('oninit');
if (component.oninit) component.oninit(vnode);
},
oncreate(vnode) {
console.log('oncreate');
if (component.oncreate) component.oncreate(vnode);
},
onupdate(vnode) {
console.log('onupdate');
if (component.onupdate) component.onupdate(vnode);
},
onremove(vnode) {
console.log('onremove');
if (component.onremove) component.onremove(vnode);
},
view(vnode) {
return component.view(vnode);
}
};
}
HOC может принимать параметры, позволяя создавать настраиваемые декораторы:
function withPermission(permission) {
return function(component) {
return {
view(vnode) {
if (!vnode.attrs.userPermissions.includes(permission)) {
return m('div', 'Доступ запрещён');
}
return component.view(vnode);
}
};
};
}
const AdminPanel = {
view() {
return m('div', 'Административная панель');
}
};
const AdminPanelWithPermission = withPermission('admin')(AdminPanel);
m.mount(document.body, {
view: () => m(AdminPanelWithPermission, { userPermissions: ['user'] })
});
Такой подход позволяет параметризовать поведение HOC, делая его универсальным и гибким.
HOC в Mithril предоставляет чистый и эффективный способ расширения компонентов, сохраняя при этом композиционную природу библиотеки и гибкость разработки интерфейсов.