Узнайте, как и когда использовать составные компоненты 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»