React 0.13 представил использование простых классов Javascript для создания компонентов, 0.14 представил «функциональные компоненты без состояния» в качестве еще одного метода их определения. Ни у одного из них нет встроенного метода для обмена поведением, как у нас с параметром mixins, переданным в createClass. Итак, теперь мы можем рассмотреть примеси как шаблон для обмена поведением и, если необходимо, придумаем что-нибудь получше. Если они не все плохие, нам нужно выяснить, как добавить их к двум новым параметрам определения компонентов.

Проблема

Статья Дэна Абромова для среды от марта 2015 года доказывает необходимость избегать примесей.

Для меня (перефразируя в крайности) выявлены ключевые проблемы:

  • миксины могут непреднамеренно скрывать/распространять источник поведения
  • миксины могут непреднамеренно скрывать/распространять источник какого-либо состояния компонента
  • миксины очень похожи на автономные определения компонентов, но они не действуют как они. См.: объединение некоторых методов жизненного цикла, а также слияние типов свойств и объектов начального состояния.

Миксины упрощают абстрагирование поведения и управления состоянием наших компонентов. Однако такая абстракция очень сложна, и мы можем случайно заставить читателей нашего кода сыграть в разочаровывающую игру в прятки. Компоненты могут стать лоскутным одеялом из различных методов, реквизитов по умолчанию и начального состояния. Конечно, этой проблемы можно избежать с помощью Discipline™, но, возможно, есть другой подход, который поощряет сохранение связанных моделей поведения вместе.

Об этом мы и поговорим: об альтернативных способах совместного использования поведения между компонентами React. Посмотрим, найдем ли мы что-нибудь полезное.

Предлагаемые в настоящее время решения: вложенность и компоненты более высокого порядка

К функциям высшего порядка относятся: «функции, которые принимают другие функции в качестве входных данных или возвращают функцию в качестве выходных данных». Пока что определение «компонентов более высокого порядка» выглядит следующим образом: функции, которые принимают компонент React в качестве входных данных и возвращают компонент React в качестве выходных данных.

Это может выглядеть примерно так:

const Loadable = (Component) => (props) => {
  if (props.loading) {
    return <div>... loading</div>
  } else { 
    return <Component { ...props } />
  }
}
const SomeComponent = (props) => (
  <div>{ props.data }</div>
)
const SomeOtherComponent = (props) => (
  <div>{ props.otherData }</div>
)
const LoadableComponent = Loadable(SomeComponent)
const AnotherLoadableComponent = Loadable(SomeOtherComponent)

Или, вы знаете:

const SomeComponent = Loadable(
  (props) => <div>{ props.data }</div>
)
const SomeOtherComponent = Loadable(
  (props) => <div>{ props.otherData }</div>
)

Тот же результат может быть достигнут с вложением:

const Loadable = (props) => {
  if (props.loading) {
    return <div>... loading</div>
  } else { 
    return props.children
  }
}
const SomeComponent = (props) => (
  <Loadable>
    <div>{ props.data }</div>
  </Loadable>
)
const SomeOtherComponent = (props) => (
  <Loadable>
    <div>{ props.otherData }</div>
  </Loadable>
)

Последний пример может подсказать вам, что это не совсем новая идея. Вы, наверное, видели такие примеры раньше, скажем, в React Redux и React CSS Transition Group.

Первые два примера выше не так распространены и могут выглядеть немного странно. Зачем нам это делать? Давайте исследовать.

Что может случиться с последним примером, если мы хотим передать компонент, отвечающий за отрисовку состояния загрузки, а не просто печатать… загрузка? Мы могли бы передать компонент, который хотим отобразить, в качестве реквизита в Loadable:

const Loadable = ({ loading, loadingState, children }) => (
  loading ? loadingState : children
)
const SomeComponent = (props) => (
  <Loadable 
    loadingState={ <div>... loading (so much has changed!)</div> }
  >
    <div>{ props.data }</div>
  </Loadable>
)

Что насчет этого:

const Loadable = (LoadingComp, ContentComp) => (props) => {
  if (props.loading) {
    return <LoadingComp { ...props } />
  } else {
    return <ContentComp { ...props } />
  }
}
const SomeComponent = Loadable(
  (props) => <div>... loading { props.resourceName }</div>,
  (props) => <div>{ props.data }</div>
)

Что, если несколько компонентов используют одно и то же состояние загрузки?

Вложение

const Loadable = ({ loading, loadingState, children }) => (
  loading ? loadingState : children
)
const ResourceNameLoadingState = ({ resourceName }) => (
  <div>... loading { resourceName }</div>
)
const SomeComponent = (props) => (
  <Loadable loadingState={ <ResourceNameLoadingState /> }>
    <div>{ props.data }</div>
  </Loadable>
)
const AnotherComponent = (props) => (
  <Loadable loadingState={ <ResourceNameLoadingState /> }>
    <div>
      <h1>I'm special</h1>
      { props.differentData }
    </div>
  </Loadable>
)

Компонент высшего порядка

const Loadable = (LoadingComp, ContentComp) => (props) => {
  if (props.loading) {
    return <LoadingComp { ...props } />
  } else {
    return <ContentComp { ...props } />
  }
}
const ResourceNameLoadingState = ({ resourceName }) => (
  <div>... loading { resourceName }</div>
)
const SomeComponent = Loadable(
  ResourceNameLoadingState,
  (props) => <div>{ props.data }</div>
)
const AnotherComponent = Loadable(
  ResourceNameLoadingState,
  (props) => <div>{ props.differentData }</div>
)

Первый из последних двух примеров мне более читабелен. Что-то, связанное с загрузкой, заключается в обертывании рендеринга моего компонента, и он принимает состояние загрузки. Я так понимаю, он, вероятно, будет отображать loadingState во время загрузки компонента.

Второй пример более неясен. Возможно, это связано с тем, что в первом примере мы получаем больше информации из имени свойства loadingState. «Первый аргумент загружаемой функции» менее информативен.

В этом случае мы могли бы предпочесть:

<WrappingComponent>
  <my-component-markup />
</WrappingComponent> 

… версия. Нам нужно имя для них… что-то связанное с «гнездованием». Матрешки?

Другой пример: трансформация реквизита

Давайте подумаем о другом примере. Что, если нам нужно последовательно преобразовать свойства, переданные компоненту. Допустим, мы хотим отфильтровать реквизиты данных, которые можно передать нескольким компонентам списка.

const filterBySearchTerm = (searchTermString, collection) => {
  const searchTermRgx = new RegExp(
    queryString.split('').join('.*'), 'gi'
  )
  return collection.filter(
    (item) => item.match(searchTermRgx)
  )
}
const SearchFilterable  = (Component) => (props) => {
  const filteredData = filterBySearchTerm(
    props.searchTerm, props.data
  )
  return <Component { ...props } data={ filteredData } />
}
const SomeListComponent = SearchFilterable(
  (props = { data: [] }) => (
    <ul>{ data.map((datum) => <li>{ datum }</i>) }</ul>
  )
)

Это проблема, с которой мы столкнулись недавно. У нас было три независимых компонента списка, в которых нужно было выполнять нечеткий поиск с использованием одних и тех же входных данных.

Давайте пойдем немного дальше: что, если в дополнение к поисковому фильтру нам нужен еще и фильтр по категориям?

// imagine that SearchFilterable has been refactored 
// to match the Regex against say a `name` key.
const fitlerByCategory = (category, collection) => (
  collection.filter((item) => item.category === category)
)
const CategoryFilterable = (Component) => (props) {
  const filteredData = fitlerByCategory(
    props.categoryFilter, props.data
  )
  return <Component { ...props } data={ filteredData } />
}
const SomeListComponent = CategoryFilterable(SearchFilterable(
  (props = { data: [] }) => (
    <ul>{ data.map((datum) => <li>{ datum }</i>) }</ul>
  )
))

Хм, это довольно шатко. Однако это начинает выглядеть как обычная проблема: funcA(funcB(params)). Это композиция функций. Посмотрим, поможет ли это.

const compose = (firstFunc, ...remainingFuncs) => (
  remainingFuncs.reduce((a, b) => (arg) => a(b(arg)), firstFunc)
)

Нам нужно, чтобы все функции, которые мы передаем для компоновки, принимали только один аргумент, а также возвращаемое значение одной функции будет использоваться в качестве входных данных для следующей. Поэтому наши функции должны принимать компонент в качестве единственного аргумента и возвращать новый компонент. Похоже, это именно то, что они уже делают :).

С композицией это может выглядеть так:

const SomeListComponent = compose(
  CategoryFilterable,
  SearchFilterable
)((props = { data: [] }) => (
  <ul>{ data.map((datum) => <li>{ datum }</i>) }</ul>
))
const SomeOtherListComponent = compose(
  CategoryFilterable,
  SearchFilterable
)((props = { data: [] }) => (
    <ol>{ data.map((datum) => <li>{ datum }</i>) }</ol>
))

Из-за того, как работает композиция функций, мы можем сделать это даже немного более общим:

const SearchAndCatFilterable = compose(
  CategoryFilterable,
  SearchFilterable
)
const SomeListComponent = SearchAndCatFilterable(
  (props = { data: [] }) => (
    <ul>{ data.map((datum) => <li>{ datum }</i>) }</ul>
  )
)
const SomeOtherListComponent = SearchAndCatFilterable(
  (props = { data: [] }) => (
    <ol>{ data.map((datum) => <li>{ datum }</i>) }</ol>
  )
)

Я думаю, это в пути. Но его все равно нет…

Кстати: меня попросили привести пример i18n… Я придумал https://gist.github.com/bradparker/11efef2fb9ac1a3b508e. Это не идеально (это очень просто), но, я думаю, иллюстрирует, что нет ничего невозможного по своей сути ни в одном из подходов к совместному использованию кода; нам просто нужно выбрать тот, который нам нравится больше всего. Или выбрать тот набор, который лучше всего решает наши конкретные задачи.

Давайте еще раз вернемся к первым двум вопросам. Устраняет ли это кровотечение поведения между миксинами и исходным компонентом? Устраняет ли это кровотечение управления состоянием?

В приведенных выше примерах один компонент-оболочка не может вызывать методы любого другого компонента, они могут взаимодействовать только через реквизиты. То же самое относится и к состоянию: ни один компонент не может напрямую обновлять состояние любого другого компонента. Тем не менее, они могут возиться с реквизитом, когда он передается по наследству. Об этом легче или сложнее рассуждать? Я не уверен.

Последний вопрос касается миксинов, которые «очень похожи» на определения компонентов. Во всех приведенных выше примерах они являются определениями компонентов. Их можно сделать с помощью createClass, как классы ES6, как функции, без разницы.

Здесь у меня нет ответа, но я надеюсь, что изучил некоторые варианты. Надеюсь, вам тоже понравилось читать.