Понимаем компоненты высшего порядка в React на реальном примере
В этой статье вы пошагово узнаете как создать компонент высшего порядка в React и как применить этот паттерн на реальном примере.
В этом руководстве мы пройдемся по концепциям, которые необходимы для создания компонентов высшего порядка. Мы применим HOC, чтобы сохранить стэйт в localStorage
, вызывая withStorage
, с помощью которого мы сможем вставить функционал в другие компоненты без дублирования логики по всему приложению.
Что такое компонент высшего порядка(HOC)
Компонент высшего порядка в React это паттерн, используемый для того, чтобы делить функционал между компонентами без повторения кода. Такие компоненты, по факту, не совсем являются компонентами, это скорее функции. Такая функция берёт компонент как аргумент и отдаёт уже готовый компонент. Она переделывает заданный компонент в другой компонент и добавляет дополнительные данные, либо фунционал. Вот краткий пример:
const NewComponent = (BaseComponent) => {
// ... создает новый компонент, обновляя старый и отдавая результат
}
Две подобных имплементации, с которыми вы возможно уже знакомы благодаря экосистеме React, это connect
из Redux и withRouter
в React Router. Функция connect
из Redux используется, чтобы раздать компонентам доступ к глобальному стейту в Redux сторе и она передаёт значения компонентам в виде пропсов. Функция withRouter
добавляет информацию о роутинге и функциональности в компонент, позволяя разработчику иметь доступ к роутеру с возможностью изменения самого роута.
Паттерн компонентов высшего порядка(HOC)
Такие компоненты это функции, которые берут компонент как аргумент и возвращают уже «переработанный» компонент. Это говорит о том, что HOC будет всегда иметь вид как тут:
import React from 'react';
const higherOrderComponent = (WrappedComponent) => {
class HOC extends React.Component {
render() {
return <WrappedComponent />;
}
}
return HOC;
};
Тут higherOrderComponent
это функция, которая берёт компонент под названием WrappedComponent
как аргумент. Мы создаём новый компонент под названием HOC, который отдаёт из рендера <WrappedComponent />
. Пока тут нет никакого функционала, этот пример просто описывает общий паттерн, которому должна следовать любая функция HOC. Мы можем вызвать HOC таким образом:
const SimpleHOC = higherOrderComponent(MyComponent);
Простой пример HOC
Теперь мы расширим наш компонент высшего порядка, чтобы добавить данные во вставленный компонент.
Наша команда выяснила, что secretToLife является число 42. Некоторые из наших компонентов должны поделиться этой информацией и нам нужно создать HOC под именем withSecretToLife
, чтобы передать его как пропс нашим компонентам.
import React from 'react';
const withSecretToLife = (WrappedComponent) => {
class HOC extends React.Component {
render() {
return (
<WrappedComponent
{...this.props}
secretToLife={42}
/>
);
}
}
return HOC;
};
export default withSecretToLife;
Обратите внимание, что этот пример практически идентичен первому. Всё, что мы сделали, так это просто проп secretToLife={42}
, который позволил обернутому компоненту получить доступ к значению через this.props.secretToLife
.
Ещё стоит обратить внимание, что используем оператор расширения на пропсах переданных компоненту. Так мы точно будем уверены в том, что любой пропс переданный обернутому компоненту будет доступен через this.props
, так же, как и если бы мы вызывали этот компонент не через компонент высшего порядка.
import React from 'react';
import withSecretToLife from 'components/withSecretToLife';
const DisplayTheSecret = props => (
<div>
The secret to life is {props.secretToLife}.
</div>
);
const WrappedComponent = withSecretToLife(DisplayTheSecret);
export default WrappedComponent;
Наш компонент WrappedComponent
, который по сути является прокачанной версией <DisplayTheSecret/>
, позволит нам получить доступ к SecretToLife
как к пропсу.
Практический пример
Теперь у нас есть четкое понимание основ создания компонентов высшего порядка и мы можем сделать такой же, но реально имеющий практическое применение в реальном приложении. HOC имеет доступ ко всем дефолтным API Реакта, включая state и методы жизненного цикла.
Функциональность нашего компонента withStorage
будет в том, чтобы сохранять/загружать стейт компонента, позволяя нам как можно быстрее получать к нему доступ и рендерить на странице.
import React from 'react';
const withStorage = (WrappedComponent) => {
class HOC extends React.Component {
state = {
localStorageAvailable: false,
};
componentDidMount() {
this.checkLocalStorageExists();
}
checkLocalStorageExists() {
const testKey = 'test';
try {
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
this.setState({ localStorageAvailable: true });
} catch(e) {
this.setState({ localStorageAvailable: false });
}
}
load = (key) => {
if (this.state.localStorageAvailable) {
return localStorage.getItem(key);
}
return null;
}
save = (key, data) => {
if (this.state.localStorageAvailable) {
localStorage.setItem(key, data);
}
}
remove = (key) => {
if (this.state.localStorageAvailable) {
localStorage.removeItem(key);
}
}
render() {
return (
<WrappedComponent
load={this.load}
save={this.save}
remove={this.remove}
{...this.props}
/>
);
}
}
return HOC;
}
export default withStorage;
На самом верху withStorage
у нас один элемент в стейте компонента, который отслеживает доступность локального хранилища в браузере. Мы используем componentDidMount
, который проверяет наличие localStorage
в checkLocalStorageExists
функции. Тут он будет тестировать сохранение элемента и ставить стейт на true
, если процесс был успешным.
Мы также добавили три функции — load
, save
и remove
. Они применяются для прямого доступа к localStorage
API, в случае его доступности. Наши три функции в HOC передались обернутому компоненту, чтобы там уже они и отработали.
Теперь мы создадим ещё один компонент, который будет внутри withStorage
. Он будет использоваться для отображения имени пользователя и его любимого фильма. Но нужно учесть, что API запрос для получения этой информации берет много времени. Мы также учитываем, что эти значения никогда не будут выставлены.
Чтобы обеспечить наилучший опыт взаимодействия с приложением, мы сделаем этот API запрос только тогда, когда значения не будут сохранены. Теперь всякий раз когда пользователь возвращается на страницу, он может получить доступ к данным незамедлительно, а не ждать ответа API.
import React from 'react';
import withStorage from 'components/withStorage';
class ComponentNeedingStorage extends React.Component {
state = {
username: '',
favoriteMovie: '',
}
componentDidMount() {
const username = this.props.load('username');
const favoriteMovie = this.props.load('favoriteMovie');
if (!username || !favoriteMovie) {
// This will come from the parent component
// and would be passed when we spread props {...this.props}
this.props.reallyLongApiCall()
.then((user) => {
this.props.save('username', user.username || '');
this.props.save('favoriteMovie', user.favoriteMovie || '');
this.setState({
username: user.username,
favoriteMovie: user.favoriteMovie,
});
});
} else {
this.setState({ username, favoriteMovie })
}
}
render() {
const { username, favoriteMovie } = this.state;
if (!username || !favoriteMovie) {
return <div>Loading...</div>;
}
return (
<div>
My username is {username}, and I love to watch {favoriteMovie}.
</div>
)
}
}
const WrappedComponent = withStorage(ComponentNeedingStorage);
export default WrappedComponent;
Внутри componentDidMount
обернутого компонента мы сначала пытаемся получить доступ к username
и favoriteMovie
из localStorage
. Если этих значений нет, то мы делаем API запрос this.props.reallyLongApiCall
. Как только получаем ответ, то сохраняем данные в локальное хранилище и обновляем стейт компонента, чтобы вывести его на экран.
Соображения по поводу HOC
Компоненты высшего порядка должны быть чистыми функциями без сайд эффектов. Они не должны делать никаких изменений, а просто формировать изначальный компонент, обертывая его другим компонентом.
Не используйте HOC в рендере компонента. Получайте доступ к HOC только за пределами определения компонента.
Статичные методы должны быть скопированы, чтобы к ним оставался доступ. Очень простой способ сделать это — использовать hoist-non-react-statics
пакет.
Ref
‘сы не передаются.