Recompose

Автор: Андрей Бакута

Композиция как способ выделения повторяющегося кода


Очень интересно следить за тем, как менялся со временем инструментарий для написания DRY кода в React. И нужно отдать должное разработчикам - ни на одном этапе развития этой библиотеки не было рекомендаций использовать наследование для решения такого рода задач. Во времена ранних версий React и React.createClass были миксины, которые представляли из себя простые JavaScript объекты. Результат их применения к React компоненте был вполне предсказуем: компонента получала новые свойства и методы, соответсвующие ключам в объекте-миксине. Чуть более нетривиальная логика присутствовала в случае, когда миксин расширял методы жизненного цикла. Практически в каждой библиотеке, которая предлагала очередную реализацию flux, был миксин, который позволял быстро подключить компоненту к стору. Практически то, что сейчас делается с помощью connect.

Потом грянул ES6. Новый синтаксис создания компонент очень сильно повлиял на способы написания переиспользуемого кода. В JavaScript нет множественного наследования, так что старые наработки с миксинами оказались бесполезны и всю идею нужно было переосмыслить. Одним из вариантов могло бы стать создание длинных цепочек наследования, но наследование очень быстро оказалось вне закона (https://reactjs.org/docs/composition-vs-inheritance.html). Facebook дал указание рекомендацию использовать композицию.

HOC как инструмент композиции


Для людей, знакомых с ООП, композиция представляет из себя возможность внутри объекта в каком-либо виде (например в поле) хранить ссылку на объект другого класса, то есть получить функциональность другого класса частично или полностью. И то, что в React называют композицией, может ввести их в замешательство. Дело в том, что не смотря на то, что React компоненты определяются как JavaScript классы, по своей сути они больше напоминают функции. А для функций естественным инструментом композиции является декоратор (функция высшего порядка, принимающая функцию и возвращающая функцию). В случае с React это будет функция, принимающая компоненту и возвращающая компоненту, она же - Higher Order Component или сокращенно HOC.

const withAuthorizedUser = Komponent => (
   props => {
      const user = getUser()
      if (user) {
         return <Komponent {…props} user={user} />
      } else {
         return <SignInPage {…props} />         
      }
   }
)
//…
export default withAuthorizedUser(Dashboard)

Возвращаясь к идее множественного наследования и миксинов, интересно посмотреть как будет выглядеть применение нескольких HOC’ов.

export default withAuthorizedUser(withRouter(withGlobalConfig(connect(Dashboard))))

С Recompose код выше можно переписать

import { compose } from ‘recompose'
const enhance = compose(
  withAuthorizedUser,
  withRouter,
  withGlobalConfig,
  connect
)
export default enhance(Dashboard)

Делаем компоненты функциональными - тренируем навыки абстракции


Recompose (https://github.com/acdlite/recompose) позиционирует себя как lodash для React, поэтому кроме возможности создавать композицию из самописных HOC’ов он также предлагает огромное количество своих на любой вкус.

У нас как-то сам собой выработался следующий стиль определения React компонент: в первую очередь пытаемся определить функциональную компоненту; если не получается, то рассматриваем возможность наследования от `PureComponent`; если и такой вариант не подходит, то определяем класс, наследованный от `Component`. То есть функции у нас в большем приоритете, чем классы.

HOC’и Recompose предоставляют возможность практически все компоненты сделать функциональными. Все, что заставляет определять наши компоненты как классы, решается использованием набора компонент высшего порядка: определение состояния и набора методов управления этим состоянием, определение обработчиков событий, выставление значений props по-умолчанию, использование методов жизненного цикла и так далее.

Типичный пример использования Recompose:

const enhance = compose(
  inject(‘rootStore'),
  withProps(({ rootStore }) => ({ uiState: rootStore.uiStateStore })),
  withHandlers({
    activateSearch: ({ uiState}) => e => {
      uiState.toggleSearchActive(!uiState.isSearchActive)
      e.currentTarget.blur()
    },
    deactivateSearch: ({ uiState }) => () => {
      uiState.toggleSearchActive(false)
    }
  }),
  observer
)
// Именованный экспорт без применения HOC’ов
// для тестирования или использования внутри стайлгайдов
export const Search = ({ activateSearch, deactivateSearch, uiState }) => (
  // … типичный jsx
)
// Экспорт по-умолчанию для использования внутри приложения
export default enhance(Search)

Одним из плюсов такого подхода является разделение поведения и преставления с возможностью переиспользования первой или второй части. Поведение у нас описывается через композицию вызовов компонент высшего порядка и сохраняется в переменную `enhance`, а представление остается в “глупой” компоненте.

В примере выше мы определили два обработчика для активации и деактивации поиска. Если бы нам понадобился еще один обработчик, например, пользовательского ввода, то добавить третий ключ `onFieldChange` после `deactivateSearch` было бы вполне рабочим решением. Но мы можем сделать еще один `withHandlers` и тем самым логически сгруппировать обработчики работающие с разными элементами (конечно за это придется платить большей вложенностью дерева React компонент).  В случае класса такую группировку можно сделать только синтаксически, расположив определения методов, работающих с одной сущностью, рядом друг с другом. Ниже пример где все функции для работы с DOM и ref вынесены в отдельную секцию.

withHandlers(() => {
  let anchorRef
  return {
    registerAnchor: () => el => {
      anchorRef = el
    },
    scrollToAnchor: () => () => {
      anchorRef.scrollIntoView()
    }
  }
})

Самым большим и неочевидным плюсом такого разделения является то, что вынесение поведения в отдельное место становится очень естественным. Если компонента является классом, то любое новое поведение (та же работа с DOM) скорее будет реализовано как новые методы класса, которые будут написаны рядом с методами, которые решают совершенно другие задачи, например, инициализацию стора. Но если все поведение компоненты уже реализовано через композицию HOC’ов, то самым естественным действием при добавлении новой функциональности будет добавить еще один HOC. Есть большой шанс что он получится достаточно универсальным (смотри пример выше) для того, чтобы его вынести в отдельный файл для последующего переиспользования. Даже если не получится переиспользовать, у нас на руках оказывается замечательный инструмент чтобы “прятать” реализацию поведения, не относящегося непосредственно к решению бизнес задачи, для которой создавалась компонента. Мне, например, не хочется видеть код обработки событий изменения размеров окна в компоненте, задача которой - отображать список товаров.

Несмотря на то, что начали набирать популярность другие паттерны (https://reactjs.org/docs/render-props.html) описания общей логики поведения, Recompose остается замечательным инструментом, который повлияет не только на стиль написания кода, но, возможно, и на ваш способ мышления.

Автор: Андрей Бакута

Комментарии

Популярные сообщения из этого блога

Стайлгайд и компонентная разработка

Прогноз погоды в консоли

Погружение в React Native: навигация, работа оффлайн, пуш нотификации