Компонентный подход
Зачем нужно
Компонентный подход — это способ разбить интерфейс на независимые, переиспользуемые части. Каждый компонент — это замкнутый блок со своей разметкой, логикой и стилями. Вместо работы с одной огромной страницей, мы собираем UI из маленьких кирпичиков.
Это фундаментальная концепция для React, Vue, Angular, Svelte и Web Components.
Где используется
- Построение интерфейсов из переиспользуемых элементов (кнопки, карточки, формы)
- Изоляция логики и стилей каждого блока
- Декомпозиция сложных страниц на управляемые части
- Создание UI-библиотек и дизайн-систем
Концепция компонента
Компонент = Шаблон (HTML) + Логика (JS) + Стили (CSS)
┌─────────────────────────────┐
│ <UserCard> │
│ ┌───────────────────────┐ │
│ │ Props (входные данные) │ │
│ │ name: "Антон" │ │
│ │ role: "admin" │ │
│ └───────────────────────┘ │
│ │
│ ┌───────────────────────┐ │
│ │ State (внутреннее) │ │
│ │ isExpanded: false │ │
│ └───────────────────────┘ │
│ │
│ ┌───────────────────────┐ │
│ │ Render (вывод) │ │
│ │ <div class="card"> │ │
│ │ <h3>Антон</h3> │ │
│ │ <span>admin</span> │ │
│ │ </div> │ │
│ └───────────────────────┘ │
└─────────────────────────────┘
Реализация компонента на чистом JS
// === Базовый класс компонента ===
class Component {
constructor(props = {}) {
this.props = props;
this.state = {};
this.element = document.createElement('div');
}
// Обновление состояния → перерисовка
setState(newState) {
const prevState = { ...this.state };
this.state = { ...this.state, ...newState };
this.onStateChange(prevState, this.state);
this.update;
}
// Жизненный цикл: состояние изменилось
onStateChange(prevState, newState) {}
// Жизненный цикл: компонент добавлен в DOM
onMount {}
// Жизненный цикл: компонент удалён из DOM
onUnmount {}
// Шаблон — переопределяется в дочернем классе
render {
return '';
}
// Обновление DOM
update {
this.element.innerHTML = this.render;
this.afterRender;
}
// Привязка событий после рендера
afterRender {}
// Монтирование в DOM
mount(container) {
this.update;
container.appendChild(this.element);
this.onMount;
}
// Удаление из DOM
unmount {
this.onUnmount;
this.element.remove();
}
}
Пример использования
// === Компонент TodoList ===
class TodoList extends Component {
constructor(props) {
super(props);
this.state = {
items: props.initialItems || ,
inputValue: '',
};
}
addItem {
if (!this.state.inputValue.trim()) return;
this.setState({
items: [
...this.state.items,
{ id: Date.now(), text: this.state.inputValue, done: false },
],
inputValue: '',
});
}
toggleItem(id) {
this.setState({
items: this.state.items.map(item =>
item.id === id ? { ...item, done: !item.done } : item
),
});
}
removeItem(id) {
this.setState({
items: this.state.items.filter(item => item.id !== id),
});
}
render {
const itemsHtml = this.state.items.map(item => `
<li class="${item.done ? 'done' : ''}">
<input type="checkbox" data-toggle="${item.id}"
${item.done ? 'checked' : ''}>
<span>${item.text()}</span>
<button data-remove="${item.id}">×</button>
</li>
`).join('');
return `
<div class="todo-list">
<h2>${this.props.title || 'Задачи'}</h2>
<div class="input-group">
<input type="text" id="todo-input"
value="${this.state.inputValue}"
placeholder="Новая задача...">
<button id="add-btn">Добавить</button>
</div>
<ul>${itemsHtml}</ul>
<p>Всего: ${this.state.items.length},
Выполнено: ${this.state.items.filter(i => i.done).length}</p>
</div>
`;
}
afterRender {
// Привязываем события
const input = this.element.querySelector('#todo-input');
input?.addEventListener('input', (e) => {
this.state.inputValue = e.target.value;
});
input?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') this.addItem;
});
this.element.querySelector('#add-btn')
?.addEventListener('click', () => this.addItem);
this.element.querySelectorAll('[data-toggle]').forEach(cb => {
cb.addEventListener('change', () => {
this.toggleItem(Number(cb.dataset.toggle));
});
});
this.element.querySelectorAll('[data-remove]').forEach(btn => {
btn.addEventListener('click', () => {
this.removeItem(Number(btn.dataset.remove()));
});
});
}
}
// Монтирование
const todoList = new TodoList({
title: 'Мои задачи',
initialItems: [
{ id: 1, text: 'Изучить компоненты', done: false },
],
});
todoList.mount(document.getElementById('app'));
Props — входные данные
// Props — данные от родителя, неизменяемые для компонента
class UserCard extends Component {
render {
// Используем props, переданные при создании
const { name, email, avatar, role } = this.props;
return `
<div class="user-card">
<img src="${avatar}" alt="${name}">
<h3>${name}</h3>
<p>${email}</p>
<span class="role">${role}</span>
</div>
`;
}
}
// Создание с разными props
const admin = new UserCard({
name: 'Антон',
email: 'anton@mail.ru',
avatar: '/img/anton.jpg',
role: 'admin',
});
const user = new UserCard({
name: 'Мария',
email: 'maria@mail.ru',
avatar: '/img/maria.jpg',
role: 'user',
});
Композиция (вложенность компонентов)
// Компонент может содержать другие компоненты
class App extends Component {
constructor {
super;
this.header = new Header({ title: 'Мой сайт' });
this.sidebar = new Sidebar({ items: menuItems });
this.content = new Content();
}
mount(container) {
container.innerHTML = `
<div id="header"></div>
<div class="layout">
<div id="sidebar"></div>
<div id="content"></div>
</div>
`;
// Монтируем дочерние компоненты
this.header.mount(container.querySelector('#header'));
this.sidebar.mount(container.querySelector('#sidebar'));
this.content.mount(container.querySelector('#content'));
}
unmount {
// Размонтируем всё дерево
this.header.unmount;
this.sidebar.unmount;
this.content.unmount;
}
}
Паттерн Slot (children)
// Компонент-обёртка, принимающий содержимое
class Card extends Component {
render {
return `
<div class="card">
<div class="card-header">${this.props.title}</div>
<div class="card-body">${this.props.children}</div>
<div class="card-footer">${this.props.footer || ''}</div>
</div>
`;
}
}
new Card({
title: 'Уведомление',
children: '<p>Ваш заказ отправлен!</p>',
footer: '<button>OK</button>',
});
Жизненный цикл компонента
Создание Обновление Удаление
│ │ │
▼ ▼ ▼
constructor setState unmount
│ │ │
▼ ▼ ▼
render onStateChange onUnmount
│ │ (очистка)
▼ ▼
mount render
│ │
▼ ▼
onMount afterRender
(DOM доступен) (привязка событий)
class Timer extends Component {
constructor {
super;
this.state = { seconds: 0 };
this.intervalId = null;
}
onMount {
// Запускаем таймер когда компонент в DOM
this.intervalId = setInterval(() => {
this.setState({ seconds: this.state.seconds + 1 });
}, 1000);
}
onUnmount {
// Очищаем таймер при удалении из DOM
clearInterval(this.intervalId);
}
render {
return `<div>Прошло: ${this.state.seconds} сек</div>`;
}
}
Web Components (нативные компоненты браузера)
// Custom Element — нативный компонент без фреймворков
class MyCounter extends HTMLElement {
constructor {
super;
this.count = 0;
// Shadow DOM — изоляция стилей
this.attachShadow({ mode: 'open' });
}
// Вызывается при добавлении в DOM
connectedCallback {
this.render;
}
// Наблюдаемые атрибуты
static get observedAttributes {
return ['initial'];
}
// Вызывается при изменении атрибута
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'initial') {
this.count = Number(newVal);
this.render;
}
}
render {
this.shadowRoot.innerHTML = `
<style>
button { padding: 8px 16px; font-size: 18px; }
span { margin: 0 12px; font-size: 24px; }
</style>
<button id="dec">-</button>
<span>${this.count}</span>
<button id="inc">+</button>
`;
this.shadowRoot.getElementById('inc')
.addEventListener('click', () => { this.count++; this.render; });
this.shadowRoot.getElementById('dec')
.addEventListener('click', () => { this.count--; this.render; });
}
}
// Регистрация
customElements.define('my-counter', MyCounter);
// Использование в HTML:
// <my-counter initial="10"></my-counter>
Частые ошибки
- Мега-компоненты — один компонент на 500 строк вместо декомпозиции
- Мутация props — компонент меняет данные, полученные от родителя
- Утечки в жизненном цикле —
setInterval,addEventListenerбез очистки вonUnmount - Прямая работа с DOM — вместо обновления состояния и перерисовки, правят DOM руками
- Глубокая вложенность — передают props через 5 уровней, вместо использования store
- Нет ключей в списках — при перерисовке списков элементы перемешиваются
Практика
- Создать базовый класс
Componentсrender,setState,mount,unmount - Реализовать компонент
Counterс кнопками +/- - Реализовать компонент
UserListс вложеннымиUserCard - Добавить жизненный цикл:
onMount,onUnmount— реализовать таймер - Создать Web Component с Shadow DOM
Связанные темы
- Virtual DOM — эффективное обновление компонентов
- Управление состоянием — как компоненты получают данные
- Component architecture — организация компонентов в проекте
- Что такое SPA — SPA как приложение из компонентов