Узнайте, как и когда использовать составные компоненты React

Итак, вы создали компонент и гордитесь им, но затем дизайн немного изменился, и вы вынуждены передать опору вроде leftAlignment={false} или isBottom для настройки вашего компонента. Хорошо, это работает, но также показывает, что в этом шаблоне есть некоторые недостатки. И для этого предназначен этот пост, чтобы вы поняли и использовали составные компоненты, чтобы вы могли избежать этого побочного эффекта передачи дополнительных свойств для обработки незначительных изменений дизайна, так что без лишних слов, давайте начнем.

Шаблон составных компонентов поначалу может показаться немного сложным, но не волнуйтесь, к концу этой статьи вы их четко поймете.

Совет. Создание компонентов React как многократно используемых фрагментов кода - отличное начало, но, безусловно, недостаточное. Используйте такие инструменты, как Bit (Github), чтобы поделиться своими компонентами с коллекцией компонентов в bit.dev. Это сделает ваши компоненты повторно используемыми в репозиториях и легко обнаруживаемыми для вас и вашей команды.

Что такое составные компоненты?

Когда несколько компонентов работают вместе, чтобы иметь общее состояние и совместно обрабатывать логику, они называются составными компонентами.

Подумайте о составных компонентах, таких как элементы <select> и <option> в HTML. По отдельности они не делают слишком многого, но вместе они позволяют создать целостный опыт. - Кент С. Доддс

Вроде <select> и <option> работают. Вы можете нажать option, и select узнает, какой из них был нажат. Они работают вместе, как и составные компоненты.

Вы когда-нибудь использовали semantic-ui-react или ant-design или любую другую библиотеку реагирования на основе пользовательского интерфейса? Если да, значит, вы что-то заметили. Как работают их компоненты. Давайте посмотрим на semantic-ui-react компонент Выпадающий список.

import React from ‘react’
import { Dropdown } from ‘semantic-ui-react’
const DropdownExampleDropdown = () => (
 <Dropdown text=’File’>
 <Dropdown.Menu>
 <Dropdown.Item text=’New’ />
 <Dropdown.Item text=’Open…’ description=’ctrl + o’ />
 <Dropdown.Item text=’Save as…’ description=’ctrl + s’ />
 <Dropdown.Item text=’Rename’ description=’ctrl + r’ />
 <Dropdown.Item text=’Make a copy’ />
 <Dropdown.Item icon=’folder’ text=’Move to folder’ />
 <Dropdown.Item icon=’trash’ text=’Move to trash’ />
 <Dropdown.Divider />
 <Dropdown.Item text=’Download As…’ />
 <Dropdown.Item text=’Publish To Web’ />
 <Dropdown.Item text=’E-mail Collaborators’ />
 </Dropdown.Menu>
 </Dropdown>
)
export default DropdownExampleDropdown

Видите ли, компонент Dropdown работает вместе с Dropdown.Menu, Dropdown.Divider и Dropdown.Item как одна команда. Теперь пользователь компонента Dropdown может решить, где он хочет разместить разделитель, и нам не нужно передавать дополнительную опору для этого.

Так как же нам создать эти составные компоненты 🤔?

Есть два основных метода создания этих компонентов.

  • React.cloneElement
  • Использование Context API (мы будем использовать хуки)

React.cloneElement

Обратите внимание на компонент semantic-ui Выпадающий список. А если не понимаешь, ничего страшного. Вот в чем суть.

import DropdownDivider from './DropdownDivider'
import DropdownItem from './DropdownItem'
import DropdownHeader from './DropdownHeader'
import DropdownMenu from './DropdownMenu'
export default class Dropdown extends Component{
  static propTypes = {}
  static Divider = DropdownDivider  
  static Header = DropdownHeader  
  static Item = DropdownItem  
  static Menu = DropdownMenu
  // functions and lifecycle hooks to handle the logic
  renderMenu = () => {
    // logic
    // uses React.cloneElement()
  }
  render() {
    // returns data to be rendered
    return this.renderMenu();
  }
}

Вышеупомянутое - это очень крошечный фрагмент исходного кода, но я хочу обратить ваше внимание на две вещи:

1. static Divider = DropdownDivider  
   static Header = DropdownHeader  
   static Item = DropdownItem  
   static Menu = DropdownMenu
2. React.cloneElement()

Этот первый пункт позволяет нам получить доступ к компонентам DropdownDivider или DropdownHeader в наших файлах, таких как эти Dropdown.Divider и Dropdown.Header.

Таким образом, мы можем использовать эти компоненты, импортировав только Dropdown компонент в наши файлы.

Второй момент - это React.cloneElement. Что оно делает?

React.cloneElement(
  element,
  [props],
  [...children]
);

Имя говорит само за себя. Он создает копию элемента и объединяет с ней новые реквизиты. И вот что говорят React docs:

Клонируйте и возвращайте новый элемент React, используя element в качестве отправной точки. Результирующий элемент будет иметь исходные реквизиты элемента с неглубоко объединенными новыми реквизитами. Новые дети заменят существующих детей. key и ref из исходного элемента будут сохранены.

Также нам нужен другой API для работы с cloneElement, React.Children.map

Согласно реакционным документам,

React.Children.map(children, function[(thisArg)])

Вызывает функцию для каждого непосредственного дочернего элемента, содержащегося в children с this, установленным в thisArg. Если children является массивом, он будет пройден, и функция будет вызываться для каждого дочернего элемента в массиве. Если дочерние элементы - null или undefined, этот метод вернет null или undefined, а не массив.

С точки зрения непрофессионала, вы можете сейчас думать об этом как о map. Хватит разговоров, давайте напишем код ✌️

Мы хотели бы создать компонент Tabs, который работал бы с другими компонентами, такими как TabPanels, TabPanel и Tab, и в целом выглядел бы примерно так

<Tabs>
   <Tab id="a">Coco</Tab>
   <Tab id="b">Up</Tab>
   <TabPanels>
      <TabPanel id="a">Miguel</TabPanel>
      <TabPanel id="b">Russell</TabPanel>
   </TabPanels>
</Tabs>

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

<Tabs>
   
   <TabPanels>
      <TabPanel id="a">Miguel</TabPanel>
      <TabPanel id="b">Russell</TabPanel>
   </TabPanels>
   <Tab id="a">Coco</Tab>
   <Tab id="b">Up</Tab>
</Tabs>

Итак, как бы мы написали этот компонент, давайте составим наш план:

Нам нужно 4 компонента Tabs TabPanels TabPanel и Tab. Мы сохраним наше состояние и функции в родительском компоненте, чтобы все дочерние компоненты могли иметь к ним доступ. Tabs является родительским компонентом, давайте сначала напишем его:

Итак, у нас есть состояние activeId, которое отслеживает текущие id и handleClick, которое изменяет activeId. Мы передаем эти два параметра дочерним компонентам в качестве свойств, используя React.Children и React.cloneElement, описанные выше.

И наш дочерний компонент будет выглядеть так:

Компонент Tab имеет кнопку при нажатии на него, он вызывает функцию handleClick с текущим идентификатором.

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

И, наконец, наш TabPanel, который будет отображать данные, когда его идентификатор совпадает с activeId

Но мы ничего подобного не делали, <Tabs.Tab> или Tabs.Panels 😒. Да, мы тоже можем это сделать, немного изменив наш Tabs компонент.

Вы можете проверить код здесь,



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

Контекстный API

Что, если у нашего родительского компонента есть состояние в контексте, и оно доступно для потомков. Наша прямая детская проблема будет решена 😃.

На этот раз я буду использовать хуки, чтобы объяснить этот подход.

Та же логика в родительском компоненте (компонент Tabs), у нас будет состояние activeId и метод handleClick. Вместо использования React.Children и React.cloneElement мы передадим наше состояние с помощью контекста. Наш контекст будет выглядеть примерно так:

export const tabContext = React.createContext({
 activeId: "",
 handleClick: () => {}
});

И наш компонент Tabs предоставит наше состояние и метод, используя контекст:

И мы получим контекст с помощью хука useContext в наших дочерних компонентах:

Логика такая же, как и раньше, как для TabPanel, так и для компонента Tab, вместо того, чтобы получать данные в качестве свойств, мы получаем их из контекста.

Контекстный подход кажется более простым и понятным, а также позволяет избежать побочных эффектов первого подхода.

Наш составной компонент можно использовать следующим образом:

<Tabs>
  <div>
    <Tabs.Tab id="a">Coco</Tabs.Tab>
    <Tabs.Tab id="b">Up</Tabs.Tab>
  </div>
  <div>
    <Tabs.TabPanel id="a">Miguel</Tabs.TabPanel>
    <Tabs.TabPanel id="b">Russell</Tabs.TabPanel>
  </div>
</Tabs>

Вот песочница для приведенного выше кода,



Заключение

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

Я надеюсь, что вам понравилась эта статья и вы узнали что-то новое, и если вы действительно хлопали сердцем, подписывайтесь на меня, чтобы получить больше контента на Medium, а также в Twitter. Пожалуйста, не стесняйтесь комментировать и спрашивать что угодно. Спасибо за чтение 🙏 💖.

Учить больше



Максимальное повторное использование кода в React
Как ускорить разработку за счет« сбора
и совместного использования компонентов React из любой кодовой базы. blog.bitsrc.io»