HOC — слишком громкое слово для простого функционального паттерна!

Месяц назад в РайффайзенБанке прошел первый фронтенд-митап, и поскольку я всего за пару дней подготовил презентацию на тему «High order components with functional patterns using Recompose», а информацию о Recompose мельком выцепил в интернете за неделю до доклада, то не успел подготовить никакого справочного материала, и даже не написал своих контактных данных в конце презентации, что было не очень хорошо. И на вопрос: «Где мы можем увидеть ваши слайды?», я замялся и ничего не ответил, за что очень сильно извиняюсь.

Хочу исправить ситуацию и написать справочный материал, а также выпустить цикл статей, в которых подробно расскажу всё то, чему было посвящено моё выступление.

?Библиотека Recompose имеет очень скудную документацию, которая не всем понятна потому, что не содержит поясняющих примеров. Попробую закрыть этот пробел, и в конце цикла мы даже коснемся RxJS.

В этой статья я расскажу о том, как композировать и декомпозировать компоненты и начну с простых коротких вопросов и определений, и покажу на примерах, как выглядит stateless-компонент, а затем — stateful-компонент.

И первый вопрос «Как называется компонент, у которого нет стейта?»



Stateless component — это компонент у которого нет стейта.

Пример (arrow function):

const stateComponent = ({name}) => <div>{name}</div>



Второй вопрос «Как называется компонент, у которого есть стейт?»


Stateful component — это компонент у которого есть стейт.

А теперь представьте, что вам не нужно пользоваться состоянием и использовать методы жизненного цикла в вашем stateful-компоненте. Точнее, вы можете это всё использовать, но вынося наружу, а затем повторно используя в разных компонентах. И делается это с помощью HOC.

Что такое HOC?

High Order Component — это функция, которая принимает компонент и возвращает новый улучшенный компонент.

Абстрактно это выглядит так:



Здесь можно увидеть, что функция принимает аргументы arg1 и arg2 и возвращает функцию, которая принимает компонент Component и возвращает новый улучшенный компонент EnhancedComponent.

Hoc может быть двух видов:

1. stateless



2. stateful



У stateful component-a есть преимущество, что мы можем указывать не только template но и методы жизненного цикла.

Пример использования:



Здесь с помощью HOC создаётся компонент Bob. В первой части HOC-a передаем в качестве аргумента объект { name: “Bob” }, а во второй части — компонент, на основе которого получим «улучшенный» компонент Bob.?

> Живой пример использования компонента высшего порядка по ссылке



Recompose


Recompose — это библиотека с уже готовыми компонентами высшего порядка. Идея в том, чтобы писать stateless-компоненты и разделять код на логические части. Пользуясь готовыми HOC-ами, вы можете отделять методы жизненного цикла, выносить бизнес логику и навешивать обработчики событий не внутри компонента, а снаружи. При этом повторно использовать всё то, чем вы пользовались, и создавать свои собственные компоненты на основе базовых.

Recompose создана Эндрю Кларком, дополнительную информацию можно найти в официальном репозитории: github.com/acdlite/recompose.

??А теперь приглядимся к слову Recompose и уберем первые две буквы. Получим метод compose, который очень часто используется для применения нескольких HOC-ов.

Давайте разберемся, что такое compose.?



Допустим у нас есть функция, которая принимает аргумент:



А что если мы захотели выполнение одной функции положить в другую:



А затем вложить результат выполнения еще и в третью функцию:



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



Compose принимает в первой части лист функций, а во второй аргумент, для которого будут выполняться функции. Причем порядок выполнения функций начинается с конца:

  1. func1
  2. func2
  3. func3

А теперь вспомним, что hoc — это функция, которая принимает компонент и возвращает новый компонент. И так как это функция, то мы можем, с помощью compose, применять несколько hoc-ов для одного компонента.?И рассмотрим простой пример, как взаимодествует compose с методами setDisplayName и setPropTypes из recompose:



setDisplayName — принимает строку и задает displayName (отображаемое имя) для компонента.
setPropTypes — принимает объект с пропсами, которые можно переиспользовать в других HOC-ах или в самом аргументах.

> Живой пример по ссылке

const { Component, PropTypes } = React;
const { compose, setDisplayName, setPropTypes } = Recompose;

const enhance = compose(
  setDisplayName('User'),
  setPropTypes({
    name: React.PropTypes.string.isRequired,
    status: React.PropTypes.string
  })
);

const User = enhance(({ name, status, dispatch }) =>
  <div className="User" onClick={
      () => dispatch({ type: "USER_SELECTED" })
    }>
    { name }: { status }
  </div>
);

console.log(User.displayName);

ReactDOM.render(
  <User name="Tim" status="active" />,
  document.getElementById('main')
);

Теперь по шагам:

1. Импортируем методы setDisplayName и setPropTypes из библиотеки Recompose, но из-за ограничений codepen.io здесь вместо импорта использована деструктуризация. В переменную enhance записываю компоненты высшего порядка setDisplayName и setPropTypes.

const { Component, PropTypes } = React;
const { compose, setDisplayName, setPropTypes } = Recompose;
const { connect } = Redux();

const enhance = compose(
  setDisplayName('User'),
  setPropTypes({
    name: React.PropTypes.string.isRequired,
    status: React.PropTypes.string
  }),
);

2. Затем применяю метод enhance для stateless компонента

const User = enhance(({ name, status, dispatch }) =>
  <div className="User">
    { name }: { status }
  </div>
);

3. Render

ReactDOM.render(
  <User name="Tim" status="active" />,
  document.getElementById('main')
);

Обратите внимание, что здесь в методе compose мы указали только первую часть, которая состоит из трех HOC-ов, и записали её в переменную enhance, а вторую часть не указали вовсе.?

Что бы понять почему мы так сделали нужно понимать, как работает метод compose и
понимать, что это функция высшего порядка:

Функция высшего порядка — это функция, которая принимает другие функции и возвращает новую функцию.

Теперь коротко опишем работу метода compose:

function compose(...funcs) {?  return funcs.reduce((a, b) => (...args) => a(b(...args)))?}

  1. В аргументы метода compose передается лист чистых функций;
  2. Далее этот лист функций перебирается с помощью метода reduce;
  3. В методе reduce в качестве аргументов передается a и b, где a — это функция аккумулятор, а b – функция выполняемая в данный момент
  4. Тело функции метода reduce — не что иное, как рекурсивная функция, которая будет перебирать массив функций с конца.

withState & withHandlers


withState — это hoc, который принимает три аргумента:

1. stateName — имя стейта, к которому можно будет обращаться;
2. stateUpdaterName — имя чистой функции, которая будет обновлять стейт;
3. initialState — исходное состояние (исходный стейт); ?

Рассмотрим пример.



Допустим у нас есть два компонента Status и Tooltip, видно, что у этих двух компонентов есть state и некоторые event handler-ы, которые меняют один и тот же state, но только при разных обстоятельствах. В компоненте Status будет появляется StatusList при клике на компонент, а в компоненте Tooltip будет появляться текст при наведении курсора на блок.



State у этих компонентов абсолютно одинаковый и имеет одинаковое исходное состояние.



Что делают обработчики событий? Каждый метод обрабатывает один и тот же флаг по-своему, но даже с такими различиями их можно объединить в одном HOC-е.

А теперь представьте, что мы можем вынести из компонента состояние и обработчики событий. Как? Ответ прост: «HOC-и из библиотеки recompose»!



Единственное отличие этих компонентов заключается в методе render. Но его можно вынести в stateless-компонент, что мы и сделаем.

Теперь вернемся к подзаголовку withState & withHandlers и сперва нам поможет withState, а после withHandlers.

withState

Создадим простой компонент, при наведении на который будет появляться Tooltip, а при клике на статус будет показываться StatusList.?

> Живой пример по ссылке

const { Component } = React;
const { compose, withState } = Recompose; // импортируем compose и withState

const StatusList = () => // StatusList - текст который будет виден при клике на слово Active
  <div className="StatusList">
    <div>pending</div>
    <div>inactive</div>
    <div>active</div>
  </div>;

// Используем hoc withState,
// где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а
// и третий аргумент initialState
const Status =  withState('isToggle', 'toggle', false)
  (({ status, isToggle, toggle }) => // 'isToggle', 'toggle' доступны в качестве аргументов
    <span onClick={ () => toggle(!isToggle) }> {/* На event onClick обрабатываем стейт компонента */}
      { status }
      { isToggle && <StatusList /> }
    </span>
  );

// Используем hoc withState,
// где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а
// и третий аргумент initialState
const Tooltip =  withState('isToggle', 'toggle', false)
  (({ text, children, isToggle, toggle }) => // 'isToggle', 'toggle' доступны в качестве аргументов
    <span>
      { isToggle && <div className="Tooltip">{ text }</div> }
      <span onMouseEnter={ () => toggle(true) } onMouseLeave={ () => toggle(false) }>{ children }</span>
     {/* На event-ы onMouseEnter и onMouseLeave обрабатываем стейт компонента */}
    </span>
  );

// Используем hoc withState,
// где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а
// и третий аргумент initialState
const User = ({ name, status }) =>
  <div className="User">
    <Tooltip text="Cool Dude!">{ name }</Tooltip>—
    <Status status={ status } />
  </div>;

const App = () =>
  <div>
    <User name="Tim" status="active" />
  </div>;

ReactDOM.render(
  <App />,
  document.getElementById('main')
);

Видно, что withState('isToggle', 'toggle', false) повторяется для двух компонентов, так давайте вынесем его в переменную withToggle:

> Живой пример по ссылке

const { Component } = React;
const { compose, withState } = Recompose; // импортируем compose и withState

const StatusList = () => // StatusList - текст который будет виден при клике на слово Active
  <div className="StatusList">
    <div>pending</div>
    <div>inactive</div>
    <div>active</div>
  </div>;

// Используем hoc withState, но уже с выносом в переменную
// где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а
// и третий аргумент initialState
const withToggle = withState('isToggle', 'toggle', false);

const Status =  withToggle(({ status, isToggle, toggle }) => // 'isToggle', 'toggle' доступны в качестве аргументов
    <span onClick={ () => toggle(!isToggle) }> {/* На event onClick обрабатываем стейт компонента */}
      { status }
      { isToggle && <StatusList /> }
    </span>
  );

// Используем hoc withState,
// где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а
// и третий аргумент initialState
const Tooltip =  withToggle(({ text, children, isToggle, toggle }) => // 'isToggle', 'toggle' доступны в качестве аргументов
    <span>
      { isToggle && <div className="Tooltip">{ text }</div> }
      <span onMouseEnter={ () => toggle(true) } onMouseLeave={ () => toggle(false) }>{ children }</span>
     {/* На event-ы onMouseEnter, onMouseLeave обрабатываем стейт компонента */}
    </span>
  );

// Используем hoc withState,
// где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а
// и третий аргумент initialState
const User = ({ name, status }) =>
  <div className="User">
    <Tooltip text="Cool Dude!">{ name }</Tooltip>—
    <Status status={ status } />
  </div>;

const App = () =>
  <div>
    <User name="Tim" status="active" />
  </div>;

ReactDOM.render(
  <App />,
  document.getElementById('main')
);

С помощью withHandlers мы можем вынести обработчики событий в hoc и вызывать в компоненте из пропсов. Рассмотрим как

const { Component } = React;
const { compose, withState, withHandlers } = Recompose; // импортируем compose, withState и withHandlers

const withToggle = compose( // теперь используем withState & withHandlers в методе compose
  withState('toggledOn', 'toggle', false),
  withHandlers({ // withHandlers принимает объект обработчиков событий
    // в каждом обработчике доступен метод toggle, который является stateUpdater-ом и обновляет стейт
    show: ({ toggle }) => (e) => toggle(true),
    hide: ({ toggle }) => (e) => toggle(false),
    toggle: ({ toggle }) => (e) => toggle((current) => !current)
  })
)

const StatusList = () => // StatusList - текст который будет виден при клике на слово Active
  <div className="StatusList">
    <div>pending</div>
    <div>inactive</div>
    <div>active</div>
  </div>;

const Status = withToggle(({ status, toggledOn, toggle }) =>
  <span onClick={ toggle }>
    { status }
    { toggledOn && <StatusList /> }
  </span>
);

const Tooltip = withToggle(({ text, children, toggledOn, show, hide }) =>
  <span>
    { toggledOn && <div className="Tooltip">{ text }</div> }
    <span onMouseEnter={ show } onMouseLeave={ hide }>{ children }</span>
  </span>
);

const User = ({ name, status }) =>
  <div className="User">
    <Tooltip text="Cool Dude!">{ name }</Tooltip>—
    <Status status={ status } />
  </div>;

const App = () =>
  <div>
    <User name="Tim" status="active" />
  </div>;

ReactDOM.render(
  <App />,
  document.getElementById('main')
);

> Живой пример по ссылке

А теперь посмотрим как у нас выглядил код до и после:





WithReducer


withReducer<S, A>(
  stateName: string,
  dispatchName: string,
  reducer: (state: S, action: A) => S,
  initialState: S | (ownerProps: Object) => S
): HigherOrderComponent

withReducer подобен методу withState и имеет схожую структуру, но стейт обновляется с помощью функции reducer-a. Рассмотрим пример:

> Живой пример по ссылке

const { Component } = React;
const { compose, withReducer, withHandlers } = Recompose; // импортируем compose, withReducer и withHandlers

const withToggle = compose(
  withReducer('toggledOn', 'dispatch', (state, action) => {
    switch(action.type) { // создаем функцию редьюсер
      case 'SHOW':
        return true;
      case 'HIDE':
        return false;
      case 'TOGGLE':
        return !state;
      default:
        return state;
    }
  }, false),
  withHandlers({
    show: ({ dispatch }) => (e) => dispatch({ type: 'SHOW' }), // пробрасываем action-ы в метод dispatch
    hide: ({ dispatch }) => (e) => dispatch({ type: 'HIDE' }),
    toggle: ({ dispatch }) => (e) => dispatch({ type: 'TOGGLE' })
  })
);


const StatusList = () => // StatusList - текст который будет виден при клике на слово Active
  <div className="StatusList">
    <div>pending</div>
    <div>inactive</div>
    <div>active</div>
  </div>;

const Status = withToggle(({ status, toggledOn, toggle }) =>
  <span onClick={ toggle }>
    { status }
    { toggledOn && <StatusList /> }
  </span>
);

const Tooltip = withToggle(({ text, children, toggledOn, show, hide }) =>
  <span>
    { toggledOn && <div className="Tooltip">{ text }</div> }
    <span onMouseEnter={ show } onMouseLeave={ hide }>{ children }</span>
  </span>
);

const User = ({ name, status }) =>
  <div className="User">
    <Tooltip text="Cool Dude!">{ name }</Tooltip>—
    <Status status={ status } />
  </div>;

const App = () =>
  <div>
    <User name="Tim" status="active" />
  </div>;

Вывод:

  1. Композиция и декомпозиция компонентов
  2. Можем пользоваться только stateless компонентами
  3. Компоненты высшего порядка позволяют создавать нечто похожее на декораторы и добавлять примеси в компонент
  4. Небольшие утилиты HOC-и могут быть скомпонованы в большие и полезные HOC-и

Комментарии (9)


  1. ThisMan
    09.06.2018 16:52

    Ничего не мог с собой поделать, но все время читал HOC по-русски)


  1. kashey
    10.06.2018 09:56

    recompose — удивительная вещь
    recompact — ничем не хуже
    Но меня не оставляет чувство, но их время уже немного прошло.


    1. saggid
      10.06.2018 11:49

      Если их время уже прошло — то каким образом вы пишете код сейчас?


      1. SPAHI4
        10.06.2018 14:01

        сейчас в моде render prop


      1. kashey
        10.06.2018 15:24
        +1

        Как ответили выше — render prop. Точнее «renderless containers», которые предоставляют свои функции и могут быть использованы «одинаково».
        Нет разницы что это Context.Consumer, React-Powerplug, VisibilitySensor или Timer — render prop унифицирует интерфейс, и задача только сделать его удобным.

        Я для «удобства» использую react-gearbox.


    1. dagen
      10.06.2018 15:07

      Они полностью взаимозаменяемые? Или есть отличия в поведении хоков с одинаковым названием?


    1. justboris
      10.06.2018 16:05

      Чувство верное. Тем более, что нам обещают более удобное функциональное API в самом React: https://twitter.com/dan_abramov/status/993104372912574464?s=21


  1. dagen
    10.06.2018 15:03

    Хорошая статья, вы меня опередили. Дополню списком плюсов, которые даёт такой подход:

    • только пропсы: все компоненты теперь работают стандартизированно только через пропсы
    • модульность: каждый хок — это чистая функция, которую можно переиспользовать в любом месте вашего приложения
    • мокирование: простое — ведь и сами компоненты чистые, и зависимости приходящие — из пропсов (при использовании с автомоками в Jest — вообще магия)
    • юнит-тесты: неприлично лёгкое покрытие юнит-тестами (вытекает из трёх пунктов выше)


  1. K3rLa3da
    11.06.2018 18:28

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