Добро пожаловать в мой первый пост о Reactjs 😄 На этот раз я попытаюсь обсудить один из наиболее широко используемых паттернов в Reactjs, а именно Compound Components
.
Compound в переводе с индонезийского означает объединять. Итак, Compound Components
— это объединение нескольких компонентов в один компонент.
Вот как это происходит с компонентами в Reactjs 🤔.
Теперь разница в том, что если этот компонент Compound Component может быть использован только в определенной области применения. В качестве примера воспользуемся обычным HTML. В HTML существуют <table />
, <tbody />
, и <thead />
. Эти теги <tbody />
и <thead />
являются частью <table />
и не могут быть использованы вне <table />
(могут, но не имеют эффекта).
В компоненте Reactjs это также можно сделать так 😁 Теперь попробуем рассмотреть пример с компонентом Modal.
Сначала мы разработаем части модала, а именно:
- Обертка
- Тело
- Нижний колонтитул
В Modal есть 3 основные части, поэтому мы можем создать компонент для каждой части с именем:
*FYI: Приведенная выше форма компонента называется Namespace Component
.
Наш дизайн завершен, теперь пришло время программирования. Прежде всего, я буду использовать Vite + React, если вы используете create-react-app, это тоже хорошо, и я также использую UI-фреймворк под названием Material UI.
*Примечание: вам не обязательно придерживаться того, что использую я, вы можете использовать CRA с React-bootstrap и NPM.
Сначала мы инициализируем проект с помощью vite:
yarn create vite modal-compound --template react
После инициализации мы открываем папку и устанавливаем зависимости:
cd modal-compound && yarn install
Если он уже установлен, запустите сервер dev:
yarn dev
Установите необходимые зависимости:
yarn add @mui/material @emotion/react @emotion/styled react-nanny
react-nanny
? что это такое? это дополнительная утилита для поиска детей детей реакта. Аналогично слоту в Vue
Если он был установлен, сначала инициализируйте App.jsx
и main.jsx
:
import { Button } from "@mui/material";
function App() {
return (
<div>
<Button variant="contained">Open Modal</Button>
</div>
);
}
export default App;
main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Инициализация завершена, теперь мы поиграем с модальным компонентом. Попробуйте создать файл в месте src/components/modal/index.jsx
, содержащий:
const ModalBody = () => {}
const ModalFooter = () => {}
const Modal = () => {}
export default Modal
Каждый компонент создан, пришло время добавить форму Namespace:
const ModalBody = () => {}
const ModalFooter = () => {}
const Modal = () => {}
Modal.Body = ModalBody
Modal.Footer = ModalFooter
export default Modal
Теперь мы добавим реквизит children
к каждой модальной части. Он становится:
import ReactDOM from "react-dom";
const ModalBody = ({ children = null }) => {
return <main>{children}</main>;
};
const ModalFooter = ({ children = null }) => {
return <footer>{children}</footer>;
};
const Modal = ({ children = null, open = false }) => {
if (!open) return null;
return ReactDOM.createPortal(
<div>{children}</div>,
document.getElementById("root")
);
};
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
export default Modal;
В компоненте <Modal />
выше я использовал react-portal, чтобы его можно было отобразить в элементе с id root
.
Теперь попробуем простую стилизацию для этого компонента <Modal />
:
import { Box, Typography } from "@mui/material";
import ReactDOM from "react-dom";
const ModalBody = ({ children = null }) => {
return <main>{children}</main>;
};
const ModalFooter = ({ children = null }) => {
return <footer>{children}</footer>;
};
const Modal = ({
children = null,
open = false,
title = "",
onClose = () => {},
}) => {
if (!open) return null;
return ReactDOM.createPortal(
<>
<Box
position="fixed"
zIndex={20}
top="50%"
left="50%"
sx={{ transform: "translate(-50%, -50%)" }}
boxShadow="rgba(149, 157, 165, 0.2) 0px 8px 24px;"
bgcolor="white"
p="1rem"
borderRadius=".5rem"
width="500px"
>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Typography variant="h1" fontSize="1.5rem" fontWeight="bold">
{title}
</Typography>
<Typography variant="caption" onClick={onClose}>
close
</Typography>
</Box>
</Box>
<Box
position="fixed"
zIndex={10}
bgcolor="rgba(0, 0, 0, 0.5)"
width="100%"
height="100%"
top={0}
left={0}
/>
</>,
document.getElementById("root")
);
};
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
export default Modal;
Теперь компонент <Modal />
будет получать реквизиты onClose
и title
. Перейдем к компоненту App.jsx
:
import { Button } from "@mui/material";
import { useState } from "react";
import Modal from "./components/modal";
function App() {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen((isOpen) => !isOpen);
return (
<div>
<Modal open={isOpen} title="Simple Modal" onClose={toggle} />
<Button variant="contained" onClick={toggle}>
Open Modal
</Button>
</div>
);
}
export default App;
Результат выглядит следующим образом:
Пришло время применить Compound Component 😄 теперь я буду использовать утилиту react-nanny
для поиска компонентов в дочерних элементах
import { Box, Typography } from "@mui/material";
import ReactDOM from "react-dom";
import { getChildByType } from "react-nanny";
const ModalBody = ({ children = null }) => {
return <main>{children}</main>;
};
const ModalFooter = ({ children = null }) => {
return <footer>{children}</footer>;
};
const Modal = ({
children = null,
open = false,
title = "",
onClose = () => {},
}) => {
const body = getChildByType(children, ModalBody);
const footer = getChildByType(children, ModalFooter);
if (!open) return null;
return ReactDOM.createPortal(
<>
<Box
position="fixed"
zIndex={20}
top="50%"
left="50%"
sx={{ transform: "translate(-50%, -50%)" }}
boxShadow="rgba(149, 157, 165, 0.2) 0px 8px 24px;"
bgcolor="white"
p="1rem"
borderRadius=".5rem"
width="500px"
>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Typography variant="h1" fontSize="1.5rem" fontWeight="bold">
{title}
</Typography>
<Typography variant="caption" onClick={onClose}>
close
</Typography>
</Box>
{body}
{footer}
</Box>
<Box
position="fixed"
zIndex={10}
bgcolor="rgba(0, 0, 0, 0.5)"
width="100%"
height="100%"
top={0}
left={0}
/>
</>,
document.getElementById("root")
);
};
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
export default Modal;
В этом коде:
const body = getChildByType(children, ModalBody);
const footer = getChildByType(children, ModalFooter);
Используется для поиска компонентов по их базовому компоненту. Например, getChildByType(children, ModalBody)
означает, что я ищу компонент ModalBody
в children
.
Потому что children
может принимать множество компонентов. Поэтому мы выбираем только те компоненты, которые необходимы. Это составные компоненты.
Он используется в App.jsx
:
import { Button, TextField } from "@mui/material";
import { useState } from "react";
import Modal from "./components/modal";
function App() {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen((isOpen) => !isOpen);
return (
<div>
<Modal open={isOpen} title="Simple Modal" onClose={toggle}>
<Modal.Body>
<TextField placeholder="Masukkan nama" variant="standard" />
</Modal.Body>
<Modal.Footer>
<Button variant="contained">Simpan</Button>
</Modal.Footer>
</Modal>
<Button variant="contained" onClick={toggle}>
Open Modal
</Button>
</div>
);
}
export default App;
Результат:
Секунда 🤔 Почему ModalBody
может быть выбран, когда мы используем Modal.Body
вместо ModalBody
. Итак, вспомните, в компоненте <Modal />
мы создали это:
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
Поэтому Modal.Body
может быть вызван
Давайте попробуем немного стилизовать:
import { Box, Typography } from "@mui/material";
import ReactDOM from "react-dom";
import { getChildByType } from "react-nanny";
const ModalBody = ({ children = null }) => {
return (
<Box component="main" my="1rem">
{children}
</Box>
);
};
const ModalFooter = ({ children = null }) => {
return <footer>{children}</footer>;
};
const Modal = ({
children = null,
open = false,
title = "",
onClose = () => {},
}) => {
const body = getChildByType(children, ModalBody);
const footer = getChildByType(children, ModalFooter);
if (!open) return null;
return ReactDOM.createPortal(
<>
<Box
position="fixed"
zIndex={20}
top="50%"
left="50%"
sx={{ transform: "translate(-50%, -50%)" }}
boxShadow="rgba(149, 157, 165, 0.2) 0px 8px 24px;"
bgcolor="white"
p="1rem"
borderRadius=".5rem"
width="500px"
>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Typography variant="h1" fontSize="1.5rem" fontWeight="bold">
{title}
</Typography>
<Typography variant="caption" onClick={onClose} color="lightgray">
close
</Typography>
</Box>
{body}
{footer}
</Box>
<Box
position="fixed"
zIndex={10}
bgcolor="rgba(0, 0, 0, 0.5)"
width="100%"
height="100%"
top={0}
left={0}
/>
</>,
document.getElementById("root")
);
};
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
export default Modal;
App.jsx
import { Button, TextField } from "@mui/material";
import { useState } from "react";
import Modal from "./components/modal";
function App() {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen((isOpen) => !isOpen);
return (
<div>
<Modal open={isOpen} title="Login" onClose={toggle}>
<Modal.Body>
<TextField
placeholder="Email"
variant="standard"
sx={{ width: "100%" }}
/>
<TextField
placeholder="Password"
variant="standard"
type="email"
sx={{ width: "100%", mt: "1rem" }}
/>
</Modal.Body>
<Modal.Footer>
<Button onClick={toggle} variant="contained">
Login
</Button>
</Modal.Footer>
</Modal>
<Button variant="contained" onClick={toggle}>
Open Modal
</Button>
</div>
);
}
export default App;
Результат:
Преимущество ✨
В чем преимущества этого составного компонента? Это выглядит так же, как использование обычного children
. Преимущество здесь:
import { Button, TextField } from "@mui/material";
import { useState } from "react";
import Modal from "./components/modal";
function App() {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen((isOpen) => !isOpen);
return (
<div>
<Modal open={isOpen} title="Login" onClose={toggle}>
<Modal.Footer> <!-- Footer terlebih dahulu -->
<Button onClick={toggle} variant="contained">
Login
</Button>
</Modal.Footer>
<Modal.Body>
<TextField
placeholder="Email"
variant="standard"
sx={{ width: "100%" }}
/>
<TextField
placeholder="Password"
variant="standard"
type="email"
sx={{ width: "100%", mt: "1rem" }}
/>
</Modal.Body>
</Modal>
<Button variant="contained" onClick={toggle}>
Open Modal
</Button>
</div>
);
}
export default App;
Вы можете сначала заполнить <Modal.Footer />
, а затем <Modal.Body />
. Если вы используете обычный children
, позиция определенно изменится. Если вы используете этот составной компонент, то даже если позиция в родительском компоненте изменится, составной компонент не изменится.
Результат:
Недостатки 🔻
Насколько я знаю, недостатком Compound Components
является долгая настройка компонентов. Мы должны определить каждую часть (Заголовок, Тело и т.д.). Так что недостатки все же есть, хе-хе.
Обложка
Пожалуй, это все, что касается Compound Components в Reactjs. Если вы считаете это полезным, пожалуйста, поделитесь этим с друзьями 😄
Увидимся в следующем уроке по React 👋
О да, для получения исходного кода посетите https://github.com/alfianandinugraha/modal-compound.