![](https://habrastorage.org/webt/vg/9v/ge/vg9vge8enxanxeeunnhvbqgrkge.jpeg)
Вносить однотипные изменения в три-четыре разных места в JS-коде — искусство, требующее концентрации внимания. Если элементов больше, поддержка кода превращается в муку. Поэтому для долгосрочных или крупных проектов следует писать код так, чтобы его можно было вынести в отдельные компоненты.
Я занимаюсь фронтенд-разработкой уже 10 лет и расскажу о применении компонентов для создания элементов фронтенда — это значительно упрощает жизнь фронтенд-разработчика.
Написано при поддержке Mail.ru Cloud Solutions.
Что такое компоненты фронтенд и зачем они нужны
HTML-теги — условно «нулевой» уровень компонентов. У каждого из них свои функции и назначение.
CSS-классы — следующий уровень абстракции, на который обычно выходят при создании даже небольшого сайта. В правилах применения стилей к CSS-классу мы описываем поведение всех элементов, которые входят в условное подмножество элементов.
Правила, применяемые к CSS-классам, а также любым другим элементам, например HTML-тегам, позволяют централизованно задавать и изменять правила отображения любого количества однотипных элементов. Есть разные инструменты для работы со стилями элементов — собственно CSS, Sass, LESS, PostCSS, и методологии применения стилей — БЭМ, SMACSS, Atomic CSS, CSS Modules, Styled components.
Собственно компоненты — это:
- однотипные элементы, которые имеют как одинаковые стили, так и одинаковую верстку (HTML) и поведение (JS);
- похожие по стилям и поведению элементы, которые незначительно отличаются друг от друга.
Сейчас развивается технология Web Components, которая позволяет делать кастомные HTML-теги, включить в верстку шаблонные куски кода. Однако компоненты стали широко применяться благодаря современным фреймворкам фронтенд-разработки, таким как Angular, Vue, React. Возможности JavaScript позволяют легко подключить компонент:
import {Header, Footer} from "./components/common";
render() {
return (
...
)
}
Все крупные проекты приходят к своей библиотеке готовых компонентов либо к использованию одной из готовых. Вопрос, когда нужно переходить от копирования кода к созданию компонентов, решается индивидуально, здесь нет однозначных рецептов.
Стоит помнить не только о написании кода, но и его поддержке. Простым copy/paste однотипной верстки и изолированием стилей в CSS-классах можно какое-то время создавать отображение без особых рисков. Но если к каждому из элементов добавляется логика поведения, написанная на JS, выгоды от переиспользования кода ощущаются буквально со 2-3 элемента, особенно когда дело касается поддержки и модификации ранее написанного кода.
Переиспользуемые компоненты React
Предположим, наше приложение стало достаточно большим, и мы решили написать свою библиотеку компонентов. Предлагаю использовать для этого популярный инструмент фронтенд-разработки React. Одно из его преимуществ — возможность просто и эффективно использовать вложенные компоненты. В коде ниже старший компонент App использует три вложенных компонента: AppHeader, Article, AppFooter:
import React from "react";
import AppHeader from "./components/AppHeader";
import Article from "./components/Article";
import AppFooter from "./components/AppFooter";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
title : "My App",
contacts : "8 800 100 20 30"
firtsArticleTitle : "Welcome",
secondArticleTitle : "Let's speak about..."
}
};
render() {
return (
<>
<AppHeader
title={this.state.title}
/>
<Article
title={this.state.firstArticleTitle}
/>
<Article
title={this.state.secondArticleTitle}
/>
<AppFooter
contacts={this.state.contacts}
/>
</>
)
}
}
Обратите внимание: теперь не требуется использовать в верстке старший оборачивающий тег — обычно это был
div
. Современный React предлагает инструмент Fragment, сокращенная запись которого <></>
. Внутри этих тегов можно использовать плоскую иерархию тегов, как в примере выше.Мы использовали три библиотечных компонента, один из которых два раза в одном блоке. Данные из родительского приложения передаются в props компонента и будут доступны внутри него через свойство
this.props
. Такой подход является типовым для React и позволяет быстро собирать отображение (View) из типовых элементов. Особенно если в вашем приложении много похожих страниц, отличающихся только содержимым статей (Model) и функциональностью.Однако нам может потребоваться модифицировать библиотечный компонент. Например, одинаковые по функциональности компоненты могут отличаться не только текстовым содержимым, но и оформлением: цвет, отступы, границы. Также можно предусмотреть разные варианты функциональности одного и того же компонента.
Ниже рассмотрен такой случай: в зависимости от наличия коллбэка наш компонент может быть «отзывчивым» или оставаться просто View для отрисовки элемента на странице:
// App.js
...
render() {
return (
<Article
text={this.state.articleText}
onClick={(e) => this.bindTap(e)}
customClass={this.state.mainCustomClass}
/>
)
}
// Article.js
import React from "react";
export default class Article extends React.Component {
constructor(props) {
super(props);
};
render() {
let cName="default";
if (this.props.customClass) cName = cName + " " this.props.customClass;
let bgColor="#fff";
if (this.props.bgColor) bgColor = this.props.bgColor;
return (
{this.props.onClick &&
<div
className={cName}
onClick={(e) => this.props.onClick(e)}
style={{background : bgColor}}
>
<p>{this.props.text}<p/>
</div>
}
{!this.props.onClick &&
<div className={cName}>
<p>{this.props.text}<p/>
</div>
}
)
}
}
В React существует еще одна техника расширения возможности компонентов. В параметрах вызова можно передавать не только данные или коллбэки, но и целиком верстку:
// App.js
...
render() {
return (
<Article
title={this.state.articleTitle}
text={
<>
<p>Please read the article</p>
<p>Thirst of all, I should say programming React is a very good practice.</p>
</>
}
/>
)
}
// Article.js
import React from "react";
export default class Article extends React.Component {
constructor(props) {
super(props);
};
render() {
return (
<div className="article">
<h2>{this.props.title}</h2>
{this.props.text}
</div>
)
}
}
Внутренняя верстка компонента будет воспроизведена целиком так, как она была передана в
props
.Чаще удобнее передавать дополнительную верстку в библиотечный компонент с помощью паттерна «вставка» и использования
this.props.children
. Такой подход лучше для модификации общих компонентов, отвечающих за типовые блоки приложения или сайта, где предполагается различное внутреннее наполнение: шапки, сайдбара, блока с рекламой и других.// App.js
...
render() {
return (
<Article title={this.state.articleTitle}>
<p>Please read the article</p>
<p>First of all, I should say programming React is a very good practice.</p>
</Article>
)
}
// Article.js
import React from "react";
export default class Article extends React.Component {
constructor(props) {
super(props);
};
render() {
return (
<div className="article">
<h2>{this.props.title}</h2>
{this.props.children}
</div>
)
}
}
Полноценные компоненты на React
Выше были рассмотрены компоненты, которые отвечают только за View. Однако нам, скорее всего, потребуется выносить в библиотеки не только отображение, но и стандартную логику обработки данных.
Давайте рассмотрим компонент Phone, который предназначен для ввода номера телефона. Он может маскировать вводимый номер c помощью подключаемой библиотеки-валидатора и сообщать старшему компоненту, что телефон введен правильно или неправильно:
// Phone.js
import React from "react";
import Validator from "../helpers/Validator";
export default class Phone extends React.Component {
constructor(props) {
super(props);
this.state = {
value : this.props.value || "",
name : this.props.name,
onceValidated : false,
isValid : false,
isWrong : true
}
this.ref = React.createRef();
};
componentDidMount = () => {
this.setValidation();
};
setValidation = () => {
const validationSuccess = (formattedValue) => {
this.setState({
value : formattedValue,
isValid : true,
isWrong : false,
onceValidated : true
});
this.props.setPhoneValue({
value : formattedValue,
item : this.state.name,
isValid : true
})
}
const validationFail = (formattedValue) => {
this.setState({
value : formattedValue,
isValid : false,
isWrong : true,
});
this.props.setPhoneValue({
value : formattedValue,
item : this.state.name,
isValid : false
})
}
new Validator({
element : this.ref.current,
callbacks : {
success : validationSuccess,
fail : validationFail
}
});
}
render() {
return (
<div className="form-group">
<labeL htmlFor={this.props.name}>
<input
name={this.props.name}
id={this.props.name}
type="tel"
placeholder={this.props.placeholder}
defaultValue={this.state.value}
ref={this.ref}
/>
</label>
</div>
)
}
}
Этот компонент уже имеет внутреннее состояние state, частью которого он может делиться с вызвавшим его внешним кодом. Другая часть остается внутри компонента, в примере выше это
onceValidated
. Таким образом, часть логики работы компонента заключена полностью в нем самом. Можно сказать, что типовое поведение не зависит от других частей приложения. Например, в зависимости от того, был номер провалидирован или нет, мы можем показывать разные тексты подсказок. Мы вынесли в отдельный переиспользуемый компонент не только отображение, но и логику обработки данных.
MV-компоненты
Если наш типовой компонент поддерживает расширенную функциональность и имеет достаточно развитую логику поведения, то стоит разделить его на два:
- «умный» для работы с данными (
Model
); - «глупый» для отображения (
View
).
Подключение будет происходить по-прежнему путем вызова одного компонента. Теперь это будет
Model
. Вторая часть — View
— будет вызываться в render()
с props
, часть из которых пришла из приложения, а другая часть уже является state самого компонента:// App.js
...
render() {
return (
<Phone
name={this.state.mobilePhoneName}
placeholder={"You mobile phone"}
/>
)
}
// Phone.js
import React from "react";
import Validator from "../helpers/Validator";
import PhoneView from "./PhoneView";
export default class Phone extends React.Component {
constructor(props) {
super(props);
this.state = {
value : this.props.value || "",
name : this.props.name,
onceValidated : false,
isValid : false,
isWrong : true
}
this.ref = React.createRef();
};
componentDidMount = () => {
this.setValidation();
};
setValidation = () => {
const validationSuccess = (formattedValue) => {
...
}
const validationFail = (formattedValue) => {
...
}
new Validator({
element : this.ref.current,
...
});
}
render() {
return (
<PhoneView
name={this.props.name}
placeholder={this.props.placeholder}
value={this.state.value}
ref={this.ref}
/>
)
}
}
// PhoneView.js
import React from "react";
const PhoneView = React.forwardRef((props, ref) => (
<div className="form-group">
<labeL htmlFor={props.name}>
<input
name={props.name}
id={props.name}
type="tel"
ref={ref}
placeholder={props.placeholder}
value={props.value}
/>
</label>
</div>
));
export default PhoneView;
Стоит обратить внимание на инструмент
React.forwardRef()
. Он позволяет создавать ref
в компоненте Phone
, но привязывать его непосредственно к элементам верстки в PhoneView
. Все манипуляции как с обычным ref
будут в таком случае доступны в Phone
. Например, если нам нужно подключить валидатор номера телефона. Другой особенностью такого подхода является максимальное упрощение
View
компонента. Фактически эта часть определена как const, без своих встроенных методов. Только верстка и подстановка данных из модели. Теперь наш переиспользуемый компонент разделен на
Model
и View
, мы можем отдельно разрабатывать код бизнес-логики и верстки. Мы также можем собирать верстку из ещё более мелких компонентов-элементов. Состояние всего приложения, выполненного на компонентах
Выше было показано, что приложение может управлять компонентами как с помощью передачи параметров или верстки, так и с помощью коллбэков.
Для успешной работы приложения требуется, чтобы верхний уровень получал значимые данные о состоянии вложенных переиспользуемых компонентов. При этом это может быть не самый верхний уровень всего приложения.
Если у нас есть блок авторизации клиента, и в нем переиспользуемые компоненты для ввода логина и пароля, всему приложению не надо знать, в каком состоянии находятся эти простые компоненты в каждый конкретный момент времени. Скорее, сам блок авторизации может вычислять новое состояние, основанное на состояниях простых переиспользуемых компонентов, и передавать его наверх: блок авторизации заполнен корректно или нет.
При большой вложенности компонентов требуется следить за организацией работы с данными, чтобы всегда знать, где находится «источник истины».
О некоторых трудностях, связанных с асинхронностью изменения состояния в React, я уже писал.
Переиспользуемые компоненты всегда должны передавать наверх через коллбэки данные, которые требуются для управления возможными блоками компонентов. Однако не нужно передавать лишние данные, чтобы не вызывать ненужных перерисовок больших частей DOM-дерева и не усложнять код обработки изменений в компонентах.
Другой подход к организации данных заключается в использовании контекста вызова компонентов. Это нативный метод React.
createContext
, доступный с версии 16.3, не путать с более ранним React getChildContext
!.. Тогда не придется передавать props через «толщу» компонентов вниз по дереву вложенности компонентов. Либо использовать специализированные библиотеки управления данными и доставки изменений, такие как Redux и Mobx (см. статью о связке Mobx + React).
Если мы построим библиотеку переиспользуемых компонентов на Mobx, у каждого из типов таких компонентов будет свой Store. То есть «источник истины» о состоянии каждого экземпляра компонента, со сквозным доступом из любого места во всем приложении. В случае с Redux и его единственным хранилищем данных все состояния всех компонентов будут доступны в одном месте.
Некоторые готовые библиотеки React-компонентов
Существуют популярные библиотеки готовых компонентов, которые, как правило, изначально были внутренними проектами компаний:
- Material-UI — набор различных компонентов, реализующих подход к дизайну Material Design от Google.
- React-Bootstrap — еще одна библиотека, реализующая популярный подход к созданию и стилизации интерфейсов. Обе библиотеки имеют огромное количество последователей по всему миру и во многом схожи по устройству: обширное API использования каждого из компонентов, хорошая документация с примерами, возможность переопределять стили элементов.
- VKUI — библиотека компонентов сети «ВКонтакте». Она используется в сторонних приложениях VK mini apps, запускаемых внутри соцсети (см. подробный материал по VK mini apps). Стили компонентов VKUI практически неотличимы от стилей нативного приложения «ВКонтакте». Это возможность использовать «бесшовный» переход от страниц ВК к страницам вашего приложения и обратно. Вместе с библиотекой vkconnect эта библиотека — сильный инструмент для построения приложений с учетом особенностей дизайна под iOS и Android.
- ARUI Feather — проект React-компонентов Альфа-банка. Это библиотека типовых расширяемых компонентов, которые банк использует для разработки интерфейсов. Библиотека позиционируется как open source, каждый может стать контрибьютором.
Все указанные библиотеки нацелены на построение верстки элементов и их стилизацию. Взаимодействие с окружением настраивают с помощью коллбэков. Поэтому если вы хотите создать полноценные переиспользуемые компоненты, описанные в третьем и четвертом пунктах статьи, придется сделать это самим. Возможно, взяв в качестве элементов View таких компонентов одну из популярных библиотек, представленных выше.
Статья написана при поддержке Mail.ru Cloud Solutions.