Месяц назад в РайффайзенБанке прошел первый фронтенд-митап, и поскольку я всего за пару дней подготовил презентацию на тему «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-и
Комментарии (9)
kashey
10.06.2018 09:56recompose — удивительная вещь
recompact — ничем не хуже
Но меня не оставляет чувство, но их время уже немного прошло.saggid
10.06.2018 11:49Если их время уже прошло — то каким образом вы пишете код сейчас?
kashey
10.06.2018 15:24+1Как ответили выше — render prop. Точнее «renderless containers», которые предоставляют свои функции и могут быть использованы «одинаково».
Нет разницы что это Context.Consumer, React-Powerplug, VisibilitySensor или Timer — render prop унифицирует интерфейс, и задача только сделать его удобным.
Я для «удобства» использую react-gearbox.
dagen
10.06.2018 15:07Они полностью взаимозаменяемые? Или есть отличия в поведении хоков с одинаковым названием?
justboris
10.06.2018 16:05Чувство верное. Тем более, что нам обещают более удобное функциональное API в самом React: https://twitter.com/dan_abramov/status/993104372912574464?s=21
dagen
10.06.2018 15:03Хорошая статья, вы меня опередили. Дополню списком плюсов, которые даёт такой подход:
- только пропсы: все компоненты теперь работают стандартизированно только через пропсы
- модульность: каждый хок — это чистая функция, которую можно переиспользовать в любом месте вашего приложения
- мокирование: простое — ведь и сами компоненты чистые, и зависимости приходящие — из пропсов (при использовании с автомоками в Jest — вообще магия)
- юнит-тесты: неприлично лёгкое покрытие юнит-тестами (вытекает из трёх пунктов выше)
K3rLa3da
11.06.2018 18:28Спасибо за статью, очень мне сейчас пригодилась. Только мне кажется что есть ошибки в картинках — к примеру в начале описания compose где две функции нужно вложить одна в одну и потом в третью
ThisMan
Ничего не мог с собой поделать, но все время читал HOC по-русски)