Хабрахабр

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

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

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

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

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

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

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

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

Пример (arrow function):

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

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


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

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

Что такое HOC?

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

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

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

stateless

2. Hoc может быть двух видов:
1. stateful

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

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

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

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

Recompose

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

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

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

А теперь приглядимся к слову Recompose и уберем первые две буквы.

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


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

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

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

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

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

  1. func1
  2. func2
  3. func3

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

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')
);

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

Импортируем методы setDisplayName и setPropTypes из библиотеки Recompose, но из-за ограничений codepen.io здесь вместо импорта использована деструктуризация. 1. В переменную 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 }),
);

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

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

Render 3.

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, который принимает три аргумента:

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

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

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

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

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

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

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

Теперь вернемся к подзаголовку 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-и
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть