Если вы когда-либо пробовали React Context API до того, как он официально стал стабильной функцией, вы, вероятно, помните все предупреждения и красные индикаторы в документации, которые отговаривали нас от его использования. Начиная с React 16.3, Context API больше не является экспериментальным - это расширенная функция, которая была разработана, чтобы помочь разработчикам избежать проблемы «Prop Drilling».

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

Контекст предназначен для обмена данными, которые можно считать« глобальными для дерева компонентов React» (React Docs)

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

В этом посте я попытаюсь описать другой подход к использованию контекста и его преимущества при рассмотрении компонентной композиции.

Меню

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

Как разработчики React, нам нравится думать о повторно используемых компонентах. Если вы знакомы с Атомным дизайном, вы, возможно, уже думаете о нашем меню как об интерфейсе, состоящем из элементов меню многократного использования. Достаточно скоро, если бы вы реализовали это меню, вы, вероятно, получили бы что-то вроде этого:

Это довольно просто - у нас есть меню, состоящее из многоразовых MenuItem. Второй MenuItem использует свойство children для передачи MenuItem, которые будут отображаться в меню как подстраницы. У каждого предмета есть 3 свойства: label, isSelected и onClick. При каждом щелчке по пункту меню мы просто проверяем, какие другие пункты меню должны быть выделены, и обновляем выбранные элементы в состоянии компонента.

Но что-то в этом коде должно нас беспокоить.

Это не похоже на «Drilling» - наше меню представляет собой «плоскую» композицию компонентов, и мы не передаем данные глубоко вниз по дереву компонентов. Вместо этого мы повторяемся. Мы повторяем способ передачи реквизита MenuItems. Что, если мы изменим то, как мы думаем о MenuItem как о отдельном компоненте?

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

Придание элементам меню контекста

Давайте еще раз посмотрим на наш пример выше. Мы собираемся использовать возможности контекста React, чтобы буквально поместить MenuItem в контекст:

Сначала мы используем React.createContext для создания Provider и Consumer. На всякий случай значение по умолчанию передается в качестве аргумента createContext, который будет использоваться, если потребитель отображается без соответствующего поставщика. Вы можете прочитать больше об этом здесь".

Затем мы создаем MenuContextProvider, который обернет наши MenuItem и позаботится о логике, необходимой для пометки элементов как выбранных. В нашем примере это itemKey в массиве selectedItems. Функция render поставщика контекста просто передает массив ключей выбранных элементов и обработчик кликов вниз, используя Provider. Эти данные будут использоваться MenuItems.

Наконец, мы немного изменим MenuItem. Каждому провайдеру нужен потребитель, поэтому обратите внимание на функцию render: она использует компонент Consumer для подписки на изменения контекста. Он также использует функцию как дочернюю для получения значения контекста, которое затем передается в renderMenuItem для его фактической визуализации, аналогично нашему исходному примеру.

Также обратите внимание, что элемент меню выбирается просто путем проверки наличия itemKey в массиве выбранных элементов меню. Решение о том, находится ли ключ в массиве, зависит от логики, управляемой самим меню.

Что мы здесь приобрели?

Определяя роль пунктов меню в меню, мы создали взаимосвязь, которая позволяет компонентам пунктов меню оставаться повторно используемыми в пределах границы - то есть меню. Поскольку в меню содержится логика выбора элемента, а также уведомления о любых щелчках по элементам (и, возможно, дополнительных событиях), мы смогли удалить как свойстваisSelected, так и onClick из MenuItem. Это делает меню расширяемым в будущем и сохраняет код чистым, читаемым и явным, сохраняя при этом любые вычисления под капотом.

Еще одна важная вещь, о которой стоит упомянуть, заключается в том, что потребители контекста читают контекст от ближайшего соответствующего поставщика, расположенного над ними в дереве компонентов. Это означает, что мы можем создавать довольно сложные меню с вложенными пунктами меню и при этом сохранять согласованность и хорошо структурированный код.

Размышляя о других сценариях использования

Вы когда-нибудь использовали переключатель? Если мы рассмотрим переключатель как отдельный компонент, легко увидеть, что он служит своей цели только как часть группы. Давайте быстро рассмотрим небольшой пример, в котором RadioButton можно составить как часть aRadioGroup:

Мы достигаем такой структуры с помощью Context аналогично тому, как это было в примере с меню выше. Компонент RadioGroup обертывает контекст React Provider, а RadioButtons потребляют данные (в данном случае выбранное значение) с помощью компонента aConsumer. RadioButton отображается в выбранном состоянии, если выбранное значение совпадает со значением, которое было передано ему с помощью свойства value. Щелчки переключателей обрабатываются путем вызова функции, переданной контекстом. Это позволяет нам инкапсулировать логику, необходимую для принятия решения о том, какой переключатель выбран как часть компонента RadioGroup. Простой!

Заключение

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

Вы можете применить эту идею в следующий раз, когда будете повторяться при создании сложной компонентной композиции. Это могут быть параметры выбора в раскрывающемся списке, различные типы полей формы в форме или даже линейные графики на диаграмме. Хотя существуют различные способы решения каждой проблемы, вы можете рассматривать контекстный API React как нечто большее, чем просто способ избежать передачи свойств дальше по дереву. Вместо этого используйте его возможности, чтобы дать контекст компонентам, которые в нем нуждаются.