Статьи

Компоненты высшего порядка: шаблон проектирования приложения React

Эта статья написана гостем Джеком Франклином . Гостевые посты SitePoint нацелены на привлечение интересного контента от известных авторов и спикеров сообщества JavaScript.

В этой статье мы обсудим, как использовать Компоненты высшего порядка, чтобы ваши приложения 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 

Чтобы узнать больше о чистых функциях, вы можете прочитать « Введение в достаточно чистое программирование » Марка Брауна.

Хотя чистые функции очень полезны и значительно упрощают отладку и тестирование приложения, иногда вам нужно создавать нечистые функции с побочными эффектами или изменять поведение существующей функции, к которой у вас нет прямого доступа (функция из библиотеки, например). Чтобы включить это, нам нужно взглянуть на функции высшего порядка.

Функции высшего порядка

Функция более высокого порядка — это функция, которая при вызове возвращает другую функцию. Часто они также принимают функцию в качестве аргумента, но это не обязательно для функции, которая должна рассматриваться как более высокого порядка.

Допустим, у нас есть наша функция 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 — это HOF, потому что он принимает функцию в качестве аргумента и возвращает новую функцию, которую мы можем вызвать. Они действительно полезны для упаковки существующих функций, которые нельзя изменить в поведении. За дополнительной информацией обращайтесь к статье М. Дэвида Грина « Функции высшего порядка в JavaScript », в которой более подробно рассматривается эта тема.

Кроме того, вы можете проверить этот CodePen , который показывает приведенный выше код в действии.

Компоненты высшего порядка

Перемещаясь в землю React, мы можем использовать ту же логику, что и выше, чтобы взять существующие компоненты React и дать им дополнительное поведение.

В этом разделе мы собираемся использовать React Router , де-факто решение для маршрутизации React. Если вы хотите начать работу с библиотекой, я настоятельно рекомендую руководство по React Router на GitHub.

Компонент React Router’s Link

React Router предоставляет компонент <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.

Функциональные компоненты без сохранения состояния

В 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 позволяет передавать дочерние элементы как обычные объекты или как дочерние элементы компонента между открывающим и закрывающим тегом.

Вы можете видеть, что это работает на Plunker .

Заказ недвижимости в Реакте

Представьте, что для одной конкретной ссылки на вашей странице вы должны использовать другое 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) {  

Функция берет компонент React (который легко определить, учитывая, что компоненты 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, вы можете создавать кодовую базу, которую проще поддерживать и работать с ней ежедневно.

Создавая компоненты высшего порядка, вы можете хранить данные, определенные только в одном месте, что упрощает рефакторинг. Создатели функций более высокого порядка позволяют сохранять большую часть данных закрытыми и предоставлять доступ к частям только тем компонентам, которые в них действительно нуждаются. Делая это, вы делаете очевидным, какие компоненты используют какие биты данных, и по мере роста вашего приложения вы найдете это полезным.

Если у вас есть какие-либо вопросы, я бы хотел их услышать. Не стесняйтесь оставлять комментарии или пинговать меня @Jack_Franklin в Twitter.

Мы объединились с Open SourceCraft, чтобы предложить вам 6 профессиональных советов от разработчиков React . Для более открытого контента, проверьте Open SourceCraft .