От автора: React, возможно, является самой популярной библиотекой JavaScript для создания пользовательских интерфейсов, и одной из причин этого является его непредвзятый характер.
Независимо от того, решите ли вы рассматривать React как фреймворк или библиотеку, можно выделить одну особенность — это его невмешательство в то, как разработчики должны создавать приложения React, что дает им, разработчикам, и командам разработчиков свободу решать, какими они хотят, чтобы были их приложения React. Поработав с разными командами и над разными приложениями, вы, вероятно, заметили некоторые общие шаблоны проектирования. В этой статье мы рассмотрим три популярных шаблона проектирования для создания приложений React.
1. Presentational and Container Component Pattern
Этот паттерн, придуман Дэном Абрамовым. В нем компоненты делятся на:
Компоненты представления: это компоненты, отвечающие за внешний вид пользовательского интерфейса. Они не имеют никаких зависимостей ни с одной частью приложения и используются для отображения данных. Примером является список:
1 2 3 4 5 6 7 8 9 10 11 |
const ItemsList = (props) => { return ( <ul> {props.items.map((item) => ( <li key={item.id}> <a href={item.url}>{item.name}</a> </li> ))} </ul> ); }; |
В приведенном выше примере наш компонент ItemsList отвечает только за отображение данных, переданных в качестве props пользовательскому интерфейсу. Презентационные компоненты также называются функциональными компонентами без сохранения состояния, но также могут быть созданы как компоненты класса и могут содержать состояние, относящееся к пользовательскому интерфейсу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class TextInput extends React.Component { constructor(props) { super(props); this.state = { value: "" }; } render() { return ( <input value={this.state.value} onChange={(event) => this.setState({ value: event.target.value })} /> ); } } |
Компоненты-контейнеры: в отличие от презентационных компонентов, компоненты-контейнеры больше отвечают за то, как все работает. Обычно это компоненты класса, которые содержат методы жизненного цикла и компоненты представления. Здесь также происходит выборка данных.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class TvShowsContainer extends React.Component { constructor(props) { super(props); this.state = { shows: [], loading: false, error: "" }; } componentDidMount() { this.setState({ loading: true, error: "" }); fetch("https://api.tvmaze.com/schedule/web?date=2020-05-29") .then((res) => res.json()) .then((data) => this.setState({ loading: false, shows: data })) .catch((error) => this.setState({ loading: false, error: error.message || error }) ); } render() { const { loading, error, shows } = this.state; return ( <div> <h1> Tv Shows </h1> {loading && <p>Loading...</p>} {!loading && shows && <ItemsList items={shows} />} {!loading && error && <p>{error}</p>} </div> ); } } |
Мы создали компонент TvShowsContainer, который извлекает данные из API, когда компонент монтируется в приведенном выше примере. Он также передает эти данные презентационному компоненту ItemsList, который мы создали ранее. Преимуществом этого шаблона является разделение задач и возможность повторного использования компонентов. Другие компоненты могут повторно использовать презентационный компонент ItemList для отображения данных, поскольку он не тесно связан с TvShowsListContainer. Посмотреть рабочее приложение можно здесь.
Обратите внимание, что Дэн не использует больше этот паттерн, поскольку он изменил свое мнение по этому вопросу. Тем не менее, вы можете найти паттерн полезным для вашего конкретного случая использования, поэтому я счел уместным упомянуть его в данной статье.
2. Provider Pattern
Одной из основных проблем, с которой сталкиваются разработчики React, является проп дриллинг (рrop drilling). Проп дриллинг — это сценарий, в котором данные (props) передаются различным компонентам, пока не доберутся до компонента, они необходимы. Хотя проп-дриллинг — это не плохо, он становится проблемой, когда несвязанные компоненты обмениваются данными, что приводит нас к идеи Provider Pattern. Этот паттерн позволяет нам хранить данные в центральном месте, например, React Context и Redux store. Provider/Store может передать эти данные любому компоненту, которому они нужны, напрямую, без проп-дриллинга.
Представьте себе внедрение темного режима для веб-приложения и создание несвязанных компонентов, реагирующих на изменение темы, вызванное другим компонентом. Мы можем добиться этого, используя шаблон Provider. Создадим объект контекста React для хранения значения темы.
1 2 3 4 5 6 |
import { createContext } from "react"; const ThemeContext = createContext({ theme: "light", setTheme: () => {} }); export default ThemeContext; |
В файле App.js мы оборачиваем импортированные компоненты с помощью ThemeContext.Provider. Это дает различным компонентам и их дочерним элементам доступ к созданному объекту контекста.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import React, { useState, useMemo } from "react"; import Header from "./Header"; import Main from "./Main"; import ThemeContext from "./context"; import "./styles.css"; export default function App() { const [theme, setTheme] = useState(""); const value = useMemo(() => ({ theme, setTheme }), [theme]); return ( <ThemeContext.Provider value={value}> <div className="container"> <Header /> <Main /> </div> </ThemeContext.Provider> ); } |
По умолчанию ThemeContext не имеет состояния и не может быть обновлен. Чтобы решить эту проблему, мы можем подключить ThemeContext к состоянию и предоставить функцию обновления в ThemeContext для изменения состояния.
Чтобы получить доступ к ThemeContext в компонентах, мы можем использовать useContext хук, представленный в React 16.9.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import { useContext } from "react"; import ThemeContext from "./context"; const Header = () => { const { theme, setTheme } = useContext(ThemeContext); const toggleTheme = () => { if (theme === "dark") { setTheme(""); return; } setTheme("dark"); return; }; return ( <header className={theme === "dark" && "dark"}> <h1> Tv Shows </h1> <button onClick={toggleTheme}>Toggle Theme</button> </header> ); }; export default Header; import { useContext } from "react"; import ThemeContext from "./context"; const Main = () => { const { theme } = useContext(ThemeContext); return ( <main className={theme === "dark" && "dark"}> <h2> {" "} {theme === "dark" ? "Dark theme enabled" : "Light theme enabled"} </h2> </main> ); }; export default Main; |
Хотя контекст упрощает передачу данных между компонентами, рекомендуется использовать этот подход с осторожностью, поскольку он затрудняет повторное использование компонентов. Вы можете получить доступ к рабочему приложению приведенного выше примера здесь. Паттерн Provider используется в React Router и React-Redux.
3. Compound Components Pattern
Составные компоненты (Compound Components)— это компоненты, которые имеют общее состояние и работают вместе для достижения общей цели. Примером могут служить HTML-элементы select и option. В сочетании вместе они создают выпадающее меню, но сами по себе они мало что делают.
Паттерн Compound Components используется в популярных библиотеках пользовательского интерфейса React, например: Ant Design и Material UI. Ниже представлена реализация компонента Menu в Material UI.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import * as React from 'react'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; export default function MaterialMenu() { return ( <div> <Button> Menu </Button> <Menu> <MenuItem>Profile</MenuItem> <MenuItem>My account</MenuItem> <MenuItem>Logout</MenuItem> </Menu> </div> ); } |
Без составных компонентов нам пришлось бы передавать props родительскому компоненту, а затем этот родительский компонент будет передавать данные дочерним компонентам.
1 |
<Menu items={['Profile','My account', 'Logout']} /> |
Вышеприведенное выглядит просто, но у нас начинаются проблемы с передачей дополнительных props дочернему компоненту. Например, представьте, что нам нужен выбранный по умолчанию пункт меню.
1 |
<Menu items={['Profile','My account', 'Logout']} defaultSelected={1} /> |
По мере поступления новых требований компонент становится слишком сложным и непригодным для использования. Паттерн Сompound Сomponent обеспечивает более чистый способ достижения необходимого результата. Существует два способа создания компонента React с использованием паттерна Сompound Сomponent: React.cloneElement и React Context. Я буду использовать подход React Context для примера ниже.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import { createContext, useState, useCallback, useMemo, useContext } from "react"; import "./styles.css"; const MenuContext = createContext(); const Menu = ({ children, defaultSelected }) => { const [selectedItem, setSelectedItem] = useState(defaultSelected); const toggleSelectedItem = useCallback( (item) => { if (item !== selectedItem) { setSelectedItem(item); return; } selectedItem(""); }, [selectedItem, setSelectedItem] ); const value = useMemo( () => ({ toggleSelectedItem, selectedItem }), [toggleSelectedItem, selectedItem] ); return ( <MenuContext.Provider value={value}> <menu className="menu">{children}</menu> </MenuContext.Provider> ); }; |
Мы создали объект контекста MenuContext для компонента Menu с помощью функции createContext, предоставляемой React Context API. Это сохранит общее состояние для компонентов Menu и MenuItem. Мы также создали состояние для выбранного пункта меню. Это позволит нам обновить контекст аналогично тому, что мы сделали в Provider Pattern, поскольку Context API по своей природе не имеет состояния.
Следующим шагом является создание компонента MenuItem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const useMenuContext = () => { const context = useContext(MenuContext); if (!context) { throw new Error( "Menu item component cannot be used outside the Menu component." ); } return context; }; const MenuItem = ({ value, children }) => { const { toggleSelectedItem, selectedItem } = useMenuContext(); return ( <button onClick={() => toggleSelectedItem(value)} id={`${value}-menu-item`} className={`menu__item ${selectedItem === value && "active"}`} > {children} </button> ); }; |
Первое, что здесь делается, — это создание пользовательского хука useMenuContext для проверки того, используется ли MenuItem вне компонента Menu, и выдачи ошибки, если это произойдет. После этого мы создаем MenuItem, используя общее состояние с компонентом Menu, чтобы определить, какой стиль применить к выбранному MenuItem, и изменить выбранный элемент при клике на элемент меню.
Подводя итог, мы соединяем эти компоненты вместе в компоненте приложения.
1 2 3 4 5 6 7 8 9 |
export default function App() { return ( <Menu defaultSelected="My account"> <MenuItem value="Profile">Profile</MenuItem> <MenuItem value="My account">My account</MenuItem> <MenuItem value="Logout">Logout</MenuItem> </Menu> ); } |
Вы можете просмотреть полный код приложения здесь.
Заключение
В этой статье мы рассмотрели различные шаблоны проектирования, которые можно использовать при создании расширяемых и повторно используемых компонентов React. Хотя это не исчерпывающий список, он может помочь с решением большинства проблем, с которыми вы, вероятно, столкнетесь при создании компонентов.
Автор: Samaila Bala
Источник: blog.openreplay.com
Редакция: Команда webformyself.
Читайте нас в Telegram, VK, Яндекс.Дзен