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

Вы когда-нибудь работали с кодовой базой или просматривали учебное пособие в Интернете и сталкивались с чем-то похожим на это:

export default withResponsive(Home)

Что ж, тогда вы уже столкнулись с Компонентой более высокого порядка, осознали вы это или нет.

Компонент более высокого порядка (сокращенно HOC) - это способ React наделять несколько компонентов повторяющимися функциями или поведением без необходимости многократно использовать один и тот же код в компоненте. Это очень умный способ следовать девизу DRY (Dont Repeat Yourself) и сократить беспорядок в коде.

HOC - это функция, которая принимает компонент в качестве аргумента и возвращает новый улучшенный компонент.

const EnhancedComponent = higherOrderComponent(OriginalComponent)

В заголовке этой статьи я использовал пример Брюса Уэйна против Бэтмена. Основное различие между ними заключается в том, что Бэтмен - это улучшенная версия Брюса с необычным костюмом и множеством гаджетов, которые он использует для борьбы с преступностью. В этом примере Брюс является исходным компонентом, а Бэтмен - расширенным компонентом. Его костюм (а также гаджеты и Бэтмобиль) - HOC.

const Batman = withBatSuit(BruceWayne)

HOC withBatSuit даст Брюсу доступ к его объекту BatSuit и превратит его в Темного рыцаря Готэма.

Итак, как именно он работает и когда его целесообразно использовать?

Когда использовать HOC

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

Теперь, до HOC, вам нужно было бы записать поведение incrementCounter в каждый из них, а затем иметь какую-то переменную состояния, которая отслеживает счетчик. Тем не менее, это требует частого копирования, вставки и повторения кода. Что, если бы мы могли дать каждому компоненту функцию incrementCounter, а также переменную state.count, а затем настраивать только действие, запускающее функцию incrementCounter в каждом из них?

Это именно то, что мы можем сделать!

Другой вариант использования - например, определение способа отображения страницы в зависимости от того, обращается ли к ней пользователь со своего ноутбука, телефона или планшета. Если ваш веб-сайт адаптируется к мобильным устройствам, существует довольно распространенный шаблон, когда вы передаете компоненту объект с именем isResponsive, который является логическим значением, указывающим истину или ложь. Вместо того, чтобы передавать это в качестве свойств каждому компоненту, которому требуется доступ, вы можете визуализировать компонент, используя HOC, называемый withResponsive, который предоставит каждому компоненту доступ к объекту isResponsive, определяющему, как он должен отображаться пользователю.

Это два распространенных использования, но вы можете использовать его буквально везде, где вам нужно несколько компонентов для общего поведения.

Реализация HOC в вашем приложении

Вернемся к нашему первому примеру добавления счетчика к нескольким компонентам вашего приложения.

Если бы мы жестко закодировали поведение счетчика в каждом приложении, вы бы получили что-то вроде этого:

App.js

function App() {
return (
  <div className="App">
    <ClickCounterWithoutHOC/>
    <HoverCounterWithoutHOC/>
  </div>
  );
}
export default App;

ClickCounterWithoutHOC

import React, {useState} from 'react'
const ClickCounterWithoutHOC = () => {
  const [count, setCount] = useState(0)
  const incrementCount = () => setCount(prevCount => prevCount + 1)
  return (
  <button onClick ={incrementCount}>
    Clicked {count} times
  </button>
  )
}

export default ClickCounterWithoutHOC

HoverCounterWithoutHOC

import React, {useState} from 'react'
const HoverCounterWithoutHOC = () => {
  const [count, setCount] = useState(0)
  const incrementCount = () => setCount(prevCount => prevCount + 1)
return (
<button onMouseOver ={incrementCount}>
    Hovered {count} times
  </button>
  )
}
export default HoverCounterWithoutHOC

Как вы видете. Этот шаблон хорош, и он работает, но мы повторяем много кода между HoverCounter и ClickCounter. Теперь представьте сценарий, в котором есть 5 или даже 50 других компонентов, которым требуется такое поведение счетчика, и внезапно у вас есть очень неэффективная кодовая база.

Вместо этого давайте создадим компонент более высокого порядка с именем withCounter.js, который добавит нам поведение count и incrementCount.

withCounter

import React, {useState} from 'react'
const withCounter = OriginalComponent => {
  const EnhancedComponent = () => {
   const [count, setCount] = useState(0)
   const incrementCount = () => setCount(prevCount => prevCount + 1)
    return (
      <OriginalComponent
        incrementCount ={incrementCount}
        count={count}
      />
    )
  }
  return EnhancedComponent
}
export default withCounter

Вы увидите, что этот компонент перемещает объявления count и increment count в withCounter.js, а затем возвращает EnhancedComponent, который представляет собой просто OriginalComponent с incrementCount и count, переданный ему в качестве свойств.

Довольно простая концепция, если вы увидите ее на практике. Позвольте мне повторить это еще раз.

Компонент более высокого порядка - это просто функция, которая принимает компонент и улучшает его, передавая ему какое-то поведение в качестве свойств. Затем компонент высшего порядка возвращает этот новый улучшенный компонент для использования.

Теперь посмотрим, как это повлияет на ClickCounter и HoverCounter.

ClickCounter

import React from 'react'
import withCounter from './withCounter'
const ClickCounter = ({count, incrementCount}) => {
  return (
    <button onClick ={incrementCount}>
      Clicked {count} times
    </button>
  )
}
export default withCounter(ClickCounter)

HoverCounter

import React from 'react'
import withCounter from './withCounter'
const HoverCounter = ({count, incrementCount}) => {
return (
    <button onMouseOver ={incrementCount}>
      Hovered {count} times
    </button>
  )
}
export default withCounter(HoverCounter)

Посмотрите, насколько он чище по сравнению с аналогами без withCounter HOC.

Все, что нам нужно сделать, это импортировать withCounter в каждый компонент, а внизу обернуть наш экспорт withCounter. Это позволит экспортировать расширенную версию нашего компонента.

Затем все, что мы делаем, это деструктурируем наши реквизиты, предоставляя нам доступ к count и incrementCount в теле нашего JSX, и все готово!

Убедитесь, что ваш файл App.js выглядит так, чтобы вы могли его протестировать:

App.js

function App() {
return (
  <div className="App">
    <ClickCounter/>
    <HoverCounter/>
  </div>
  );
}
export default App;

И вот оно! Вот ссылка на мой репозиторий GitHub, если вы хотите получить доступ к любому коду из этой статьи, чтобы поиграть с собой!