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

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

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

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

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

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

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

etudijqah4_n63t6byltmpwms-s.png

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

Пример (arrow function):

const stateComponent = ({name}) => 
{name}

716aocw31ff7s8v5al84fg9fvy8.png

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

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

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

Что такое HOC?

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

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

kskcopwtrgnnboiveivmopby1w4.png

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

Hoc может быть двух видов:
1. stateless
vnz78kuhx-gtf34rjcpd4d3pjdm.png
2. stateful
qtkchqenwdjafy_yryytz2fodna.png

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

Пример использования:
japjbxjsu8zznfkroqrf-ybomei.png

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


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

lsrdbxljjvt7chnn1w1bnfzcarc.png

Recompose

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

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



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

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

oj9sga1wbmi-l6b73q5xbtqyeu4.png

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

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

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

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

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

  1. func1
  2. func2
  3. func3

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

ffoxj2gopirup1zfmvkf_64qrm0.png

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 }) =>
  
dispatch({ type: "USER_SELECTED" }) }> { name }: { status }
); console.log(User.displayName); ReactDOM.render( , 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 }) =>
  
{ name }: { status }
);

3. Render

ReactDOM.render(
  ,
  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 — исходное состояние (исходный стейт); 


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

igy6nwx7izrzpktpbsy-kp-jmdg.png

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

eccmjzr9or6xxm_ug6_aoy1aviy.png

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

x9zevnkrcfbhaqybceubsaiqaf4.png

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

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

6vwgxrnqmylgyzm5_rj5tor-bxq.png

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

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

withState

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

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

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

const StatusList = () => // StatusList - текст который будет виден при клике на слово Active
  
pending
inactive
active
; // Используем hoc withState, // где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а // и третий аргумент initialState const Status = withState('isToggle', 'toggle', false) (({ status, isToggle, toggle }) => // 'isToggle', 'toggle' доступны в качестве аргументов toggle(!isToggle) }> {/* На event onClick обрабатываем стейт компонента */} { status } { isToggle && } ); // Используем hoc withState, // где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а // и третий аргумент initialState const Tooltip = withState('isToggle', 'toggle', false) (({ text, children, isToggle, toggle }) => // 'isToggle', 'toggle' доступны в качестве аргументов { isToggle &&
{ text }
} toggle(true) } onMouseLeave={ () => toggle(false) }>{ children } {/* На event-ы onMouseEnter и onMouseLeave обрабатываем стейт компонента */}
); // Используем hoc withState, // где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а // и третий аргумент initialState const User = ({ name, status }) =>
{ name }
; const App = () =>
; ReactDOM.render( , document.getElementById('main') );

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

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

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

const StatusList = () => // StatusList - текст который будет виден при клике на слово Active
  
pending
inactive
active
; // Используем hoc withState, но уже с выносом в переменную // где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а // и третий аргумент initialState const withToggle = withState('isToggle', 'toggle', false); const Status = withToggle(({ status, isToggle, toggle }) => // 'isToggle', 'toggle' доступны в качестве аргументов toggle(!isToggle) }> {/* На event onClick обрабатываем стейт компонента */} { status } { isToggle && } ); // Используем hoc withState, // где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а // и третий аргумент initialState const Tooltip = withToggle(({ text, children, isToggle, toggle }) => // 'isToggle', 'toggle' доступны в качестве аргументов { isToggle &&
{ text }
} toggle(true) } onMouseLeave={ () => toggle(false) }>{ children } {/* На event-ы onMouseEnter, onMouseLeave обрабатываем стейт компонента */}
); // Используем hoc withState, // где первый аргумент isToggle — имя стейта, второй toggle - имя функции stateUpdater-а // и третий аргумент initialState const User = ({ name, status }) =>
{ name }
; const App = () =>
; ReactDOM.render( , 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
  
pending
inactive
active
; const Status = withToggle(({ status, toggledOn, toggle }) => { status } { toggledOn && } ); const Tooltip = withToggle(({ text, children, toggledOn, show, hide }) => { toggledOn &&
{ text }
} { children }
); const User = ({ name, status }) =>
{ name }
; const App = () =>
; ReactDOM.render( , document.getElementById('main') );


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

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

7lkgmzlg5hoc4vp0k581zhla4kw.png

i3yhm6pkzi6ms2gfor5nh12xjke.png

WithReducer

withReducer(
  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
  
pending
inactive
active
; const Status = withToggle(({ status, toggledOn, toggle }) => { status } { toggledOn && } ); const Tooltip = withToggle(({ text, children, toggledOn, show, hide }) => { toggledOn &&
{ text }
} { children }
); const User = ({ name, status }) =>
{ name }
; const App = () =>
;

Вывод:

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

© Habrahabr.ru