HOC — слишком громкое слово для простого функционального паттерна!
Месяц назад в РайффайзенБанке прошел первый фронтенд-митап, и поскольку я всего за пару дней подготовил презентацию на тему «High order components with functional patterns using Recompose», а информацию о Recompose мельком выцепил в интернете за неделю до доклада, то не успел подготовить никакого справочного материала, и даже не написал своих контактных данных в конце презентации, что было не очень хорошо. И на вопрос: «Где мы можем увидеть ваши слайды?», я замялся и ничего не ответил, за что очень сильно извиняюсь.
Хочу исправить ситуацию и написать справочный материал, а также выпустить цикл статей, в которых подробно расскажу всё то, чему было посвящено моё выступление.
Библиотека Recompose имеет очень скудную документацию, которая не всем понятна потому, что не содержит поясняющих примеров. Попробую закрыть этот пробел, и в конце цикла мы даже коснемся RxJS.
В этой статья я расскажу о том, как композировать и декомпозировать компоненты и начну с простых коротких вопросов и определений, и покажу на примерах, как выглядит stateless-компонент, а затем — stateful-компонент.
И первый вопрос «Как называется компонент, у которого нет стейта?»

Stateless component — это компонент у которого нет стейта.
Пример (arrow function):

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

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 — это библиотека с уже готовыми компонентами высшего порядка. Идея в том, чтобы писать stateless-компоненты и разделять код на логические части. Пользуясь готовыми HOC-ами, вы можете отделять методы жизненного цикла, выносить бизнес логику и навешивать обработчики событий не внутри компонента, а снаружи. При этом повторно использовать всё то, чем вы пользовались, и создавать свои собственные компоненты на основе базовых.
Recompose создана Эндрю Кларком, дополнительную информацию можно найти в официальном репозитории: github.com/acdlite/recompose.
А теперь приглядимся к слову Recompose и уберем первые две буквы. Получим метод compose, который очень часто используется для применения нескольких HOC-ов.
Давайте разберемся, что такое compose.

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

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

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

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

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

setDisplayName — принимает строку и задает displayName (отображаемое имя) для компонента.
setPropTypes — принимает объект с пропсами, которые можно переиспользовать в других HOC-ах или в самом аргументах.
→ Живой пример по ссылке
Теперь по шагам:
1. Импортируем методы setDisplayName и setPropTypes из библиотеки Recompose, но из-за ограничений codepen.io здесь вместо импорта использована деструктуризация. В переменную enhance записываю компоненты высшего порядка setDisplayName и setPropTypes.
2. Затем применяю метод enhance для stateless компонента
3. Render
Обратите внимание, что здесь в методе compose мы указали только первую часть, которая состоит из трех HOC-ов, и записали её в переменную enhance, а вторую часть не указали вовсе.
Что бы понять почему мы так сделали нужно понимать, как работает метод compose и
понимать, что это функция высшего порядка:
Функция высшего порядка — это функция, которая принимает другие функции и возвращает новую функцию.
Теперь коротко опишем работу метода compose:
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.
→ Живой пример по ссылке
Видно, что withState('isToggle', 'toggle', false) повторяется для двух компонентов, так давайте вынесем его в переменную withToggle:
→ Живой пример по ссылке
С помощью withHandlers мы можем вынести обработчики событий в hoc и вызывать в компоненте из пропсов. Рассмотрим как
→ Живой пример по ссылке
А теперь посмотрим как у нас выглядил код до и после:


withReducer подобен методу withState и имеет схожую структуру, но стейт обновляется с помощью функции reducer-a. Рассмотрим пример:
→ Живой пример по ссылке
Вывод:
Месяц назад в РайффайзенБанке прошел первый фронтенд-митап, и поскольку я всего за пару дней подготовил презентацию на тему «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 принимает в первой части лист функций, а во второй аргумент, для которого будут выполняться функции. Причем порядок выполнения функций начинается с конца:
- func1
- func2
- 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))) }
- В аргументы метода compose передается лист чистых функций;
- Далее этот лист функций перебирается с помощью метода reduce;
- В методе reduce в качестве аргументов передается a и b, где a — это функция аккумулятор, а b – функция выполняемая в данный момент
- Тело функции метода 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>;
Вывод:
- Композиция и декомпозиция компонентов
- Можем пользоваться только stateless компонентами
- Компоненты высшего порядка позволяют создавать нечто похожее на декораторы и добавлять примеси в компонент
- Небольшие утилиты HOC-и могут быть скомпонованы в большие и полезные HOC-и
