Подписывайтесь на мой твиттер, там всегда что-нибудь интересное!

Понимаем компоненты высшего порядка в React на реальном примере

В этой статье вы пошагово узнаете как создать компонент высшего порядка в React и как применить этот паттерн на реальном примере.

Перевод Understanding React Higher-Order Components by Example

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

Мы также добавили три функции — loadsave и 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‘сы не передаются.