Компоненты высшего порядка в React
Оригинал статьи: Higher Order Components: A React Application Design Pattern
Оглавление:- Чистые функции
- Функции высшего порядка
- Компоненты высшего порядка
- Функциональные компоненты без состояния
- Улучшенные компоненты высшего порядка
- Создатели компонентов высшего порядка
- Заключение
В этой статье мы обсудим, как использовать компоненты высшего порядка, чтобы сохранять приложения React аккуратными, хорошо структурированными и простыми в поддержке. Мы будем говорить о том, как чистые функции делают код ясным и как эти принципы можно применить к компонентам React.
Чистые функции
Функция является чистой, если соответствует следующим правилам:
- все данные, с которыми она работает, объявляются как аргументы;
- она не изменяет переданные ей или любые иные данные (это обычно называется побочные эффекты);
- с одинаковыми аргументами она всегда дает одинаковый результат.
Например, функция add
в примере ниже является чистой:
function add(x, y) {
return x + y;
}
А вот функция badAdd
чистой не является:
var y = 2;
function badAdd(x) {
return x + y;
}
Эта функция не чистая, потому что использует данные, которые не были ей переданы напрямую. В результате можно вызвать эту функцию с теми же входными данными и получить другой результат:
var y = 2;
badAdd(3) // 5
y = 3;
badAdd(3) // 6
Больше о чистых функциях можно прочитать из статьи Марка Брауна “An introduction to reasonably pure programming”.
Хотя чистые функции очень полезны и значительно облегчают отладку и тестирование приложения, временами вам приходиться создавать не чистые функции с побочными эффектами или же модифицировать поведение существующей функции, к которой у вас нет доступа напрямую (это может быть функция из сторонней библиотеки, например). Для того, чтобы делать это, нам надо взглянуть на функции высшего порядка.
Функции высшего порядка
Функция высшего порядка это функция, которая возвращает другую функцию. Часто они также принимают функции в качестве аргументов, но это не является обязательным свойством функций высшего порядка.
Предположим, у нас есть функция add
из примера выше и мы хотим написать какой-либо код так, чтобы при ее вызове логировать результат в консоль перед его возвращением. Мы не можем редактировать функцию add
, поэтому вместо этого мы создадим новую функцию.
function addAndLog(x, y) {
var result = add(x, y);
console.log('Result', result);
return result;
}
Итак, мы решили, что логирование результатов функций полезно и теперь мы хотим проделать то же самое с функцией subtract
. Но вместо того, чтобы дублировать код выше, мы можем написать функцию высшего порядка, которая может принимать функцию как аргумент и возвращать новую функцию, вызывающую переданную функцию и логирующую результат перед его возвращением:
function logAndReturn(func) {
return function() {
var args = Array.prototype.slice.call(arguments);
var result = func.apply(null, args);
console.log('Result', result);
return result;
}
}
Теперь мы можем взять эту функцию и использовать ее для логирования функций add
и subtract
:
var addAndLog = logAndReturn(add);
addAndLog(4, 4) // 8 is returned, ‘Result 8’ is logged
var subtractAndLog = logAndReturn(subtract);
subtractAndLog(4, 3) // 1 is returned, ‘Result 1’ is logged;
Функция logAndReturn
относится к функциям высшего порядка так как принимает функцию как аргумент и возвращает новую функцию, которую мы можем вызвать. Это действительно полезно для оборачивания существующих функций, поведение которых вы не можете изменить. Больше информации об этом вы можете почерпнуть из статьи Дэвида Грина “Higher-Order Functions in JavaScript”, в которой тема раскрывается подробно.
Также вы можете рассмотреть действие кода из примеров в этом демо на Codepen.
Компоненты высшего порядка
Перенесясь в страну React, мы можем использовать ту же логику, чтобы брать существующие компоненты React и добавлять им дополнительное поведение.
В этом разделе мы будем использовать роутер React, это фактически официальный плагин React для маршрутизации. Если вы хотите для начала ознакомиться с ним, я рекомендую учебный курс по роутеру React на GutHub.
Компонент <Link>
роутера React
В роутере React есть компонент <Link>
, используемый для ссылок между страницами в приложении React. Одним из свойств, которые принимает компонент <Link>
, является activeClassName
. Когда у <Link>
есть это свойство и оно активно в текущий момент (пользователь перешел на соответствующий URL), компонент получит этот класс, позволяя разработчику добавить нужные стили.
Это действительно полезная возможность и мы решили постоянно использовать ее в нашем гипотетическом приложении. Однако сделав так, мы быстро обнаружим, что все наши компоненты <Link>
слишком многословны:
<Link to="/" activeClassName="active-link">Home</Link>
<Link to="/about" activeClassName="active-link">About</Link>
<Link to="/contact" activeClassName="active-link">Contact</Link>
Обратите внимание, что мы каждый раз повторяем названия классов. Это не только делает наши компоненты слишком многословными, но также означает, что если мы решим изменить название класса, нам придется делать это сразу во многих местах.
Вместо этого мы можем написать компонент, оборачивающий компонент <Link>
:
var AppLink = React.createClass({
render: function() {
return (
<Link to={this.props.to} activeClassName="active-link">
{this.props.children}
</Link>;
);
}
});
Теперь мы можем использовать этот компонент, делая наши ссылки более аккуратными:
<AppLink to="/">Home</AppLink>
<AppLink to="/about">About</AppLink>
<AppLink to="/contact">Contact</AppLink>
Вы можете посмотреть работу этого примера на Plunker.
В экосистеме React такие компоненты известны как компоненты высшего порядка, так как они принимают существующий компонент и слегка манипулируют им, не меняя существующий компонент. Вы также можете думать о них, как о компонентах-обертках, но название “компоненты высшего порядка” является общеупотребимым.
Функциональные компоненты без состояния
В версии React 0.14 появилась поддержка функциональных компонентов без состояния. Это компоненты, обладающие следующими характеристиками:
- у них нет никакого состояния;
- они не используют методы жизненного цикла React (такие как
componentWillMount()
); - у них есть только метод
render
и ничего больше.
Когда компонент сочетает в себе все это, мы можем определить его как функцию, вместо использования React.createClass
(или class App extends React.Component
, если вы используете классы ES2015). Например, следующие два выражения создают идентичные компоненты:
var App = React.createClass({
render: function() {
return <p>My name is { this.props.name }</p>;
}
});
var App = function(props) {
return <p>My name is { props.name }</p>;
}
В функциональном компоненте без состояния вместо ссылки на this.props
мы передаем props
. Вы можете узнать больше об этом из документации React.
Так как компоненты высшего порядка часто обертывают существующий компонент, вы можете определить их как функциональные компоненты. В оставшейся части статьи я продемонстрирую это.
Улучшенные компоненты высшего порядка
Компонент из примера работает, но мы можем сделать намного лучше. Созданный нами компонент AppLink
не вполне соответствует своему назначению.
Принятие неограниченного количества свойств
Компонент AppLink
ожидает только два свойства:
this.props.to
это URL ссылки, по которому переходит пользователь;this.props.children
это текст, показываемый пользователю.
Однако компонент <Link>
принимает намного больше свойств и может быть ситуация, когда вам надо будет передать дополнительные свойства, кроме тех двух, которые передаются всегда. Мы не сделали AppLink
расширяемым, жестко указав нужные нам свойства.
Расширение JSX
JSX, HTML-подобный синтаксис для задания элементов React, поддерживает оператор расширения для передачи объекта в компонент как набора свойств. Вот образец кода, демонстрирующий, как это делается:
var props = { a: 1, b: 2};
<Foo a={props.a} b={props.b} />
<Foo {...props} />
Использование {...props}
берет каждый ключ в объекте и передает его в Foo
как индивидуальное свойство.
Мы можем использовать этот трюк с <AppLink>
, таким образом получив поддержку любого произвольного свойства <Link>
. Также мы оберегаем себя на будущее — если в <Link>
со временем добавятся новые свойства, наш компонент уже будет поддерживать их. Пока мы продолжим им заниматься, я собираюсь превратить <AppLink>
в функциональный компонент.
var AppLink = function(props) {
return <Link {...props} activeClassName="active-link" />;
}
Теперь <AppLink>
будет принимать все свойства и передавать их дальше. Отметьте, что мы также можем использовать самозакрывающуюся форму тега вместо явной ссылки на {props.children}
внутри тегов <Link>
. React позволяет передавать children
как обычное свойство или как дочерние элементы компонента между открывающим и закрывающим тегами.
Порядок свойств в React
Представьте, что для какой-нибудь особой ссылки на странице вам надо использовать другое значение activeClassName
. Вы пытаетесь передать его в <AppLink>
также как и остальные свойства:
<AppLink to=“/special-link” activeClassName=“special-active”>Special Secret Link</AppLink>
Однако это не работает. Причина состоит в порядке свойств при рендеринге компонента <Link>
:
return <Link {...props} activeClassName="active-link" />;
Когда вы много раз передаете одинаковое свойство в компонент React, приоритет имеет последняя декларация. Это означает, что последняя декларация activeClassName=“active-link”
будет всегда выигрывать, так как расположена после {...this.props}
. Для исправления этого мы можем переупорядочить свойства, расширив this.props
. Это значит, что мы задаем разумные настройки по умолчанию, которые хотим использовать, но у пользователя есть возможность поменять их в случае необходимости:
return <Link activeClassName="active-link" {...props} />;
Опять-таки, вы может оценить изменения в действии на Plunker.
Создавая компоненты высшего порядка, оборачивающие существующие компоненты и добавляющие им поведение, мы сохраняем нашу кодовую базу чистой и охраняем ее от будущих изменений, не повторяя свойства и храня их значения только в одном месте.
Создатели компонентов высшего порядка
Зачастую у вас есть несколько компонентов, которые вам надо обернуть, добавив одинаковое поведение. Это очень похоже на наш пример с оборачиванием функций add
и subtract
для логирования.
Предположим, в вашем приложении есть объект, содержащий информацию о текущем пользователе, вошедшем в систему. Вам нужно, чтобы некоторые компоненты React получили доступ к этой информации, но вместо того, чтобы просто предоставить такой доступ сразу всем компонентам, вы хотите сделать это выборочно.
Это решается путем создания функции, которую мы можем вызвать вместе с компонентом React. Функция будет возвращать новый компонент React, который выведет переданный компонент, добавив ему дополнительное свойство, предоставляющее доступ к информации пользователя.
Это звучит несколько сложно, проще показать на примере кода:
function wrapWithUser(Component) {
// Информация, которую мы не хотим делать общедоступной
var secretUserInfo = {
name: 'Jack Franklin',
favouriteColour: 'blue'
};
// возвращаем новый сгенерированный компонент React
// используя функциональный компонент без состояния
return function(props) {
// передаем переменную с данными пользователя как свойство
// вместе с остальными переданными свойствами
return <Component user={secretUserInfo} {...props} />
}
}
Функция принимает компонент React (который легко заметить по заглавной букве в начале) и возвращает новую функцию, которая будет выводить компонент, добавив ему свойство user
со значением secretUserInfo
.
Теперь возьмем компонент <AppHeader>
, которому нужен доступ к этой информации для вывода данных залогиненного пользователя:
var AppHeader = function(props) {
if (props.user) {
return <p>Logged in as {props.user.name}</p>;
} else {
return <p>You need to login</p>;
}
}
Финальный шаг это соединение компонента с переданным свойством this.props.user
. Мы можем создать новый компонент, передавая его в нашу функцию wrapWithUser
.
var ConnectedAppHeader = wrapWithUser(AppHeader);
Теперь у нас есть компонент <ConnectedAppHeader>
, который выводится и обладает доступом к объекту user
.
Рабочий пример выложен на Codepen.
Я выбрал названием для компонента ConnectedAppHeader
, так как рассматриваю его соединенным с определенными данными, доступ к которым есть не у каждого компонента.
Это паттерн очень распространен в библиотеках React, особенно в Redux, поэтому знание о том, как он работает и почему применяется, поможет вам при росте вашего приложения и упростит работу со сторонними библиотеками, в которых он применяется.
Заключение
Эта статья показала, как применяя принципы функционального программирования, такие как чистые функции и компоненты высшего порядка в React, вы можете создать кодовую базу, которую легко поддерживать и с которой легко работать на ежедневной основе.
Создавая компоненты высшего порядка вы можете сохранять данные, определенными в одном месте, облегчая тем самым рефакторинг. Создатели функций высшего порядка позволяют сохранять данные изолированными и открывать их только тем компонентам, которые действительно нуждаются в этом. Таким образом вы делаете очевидным, какими компонентами используются какие данные, и при росте приложения вы ощутите преимущества этого подхода.