Создание составных компонентов в Reactjs

Добро пожаловать в мой первый пост о 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.

Оцените статью
Procodings.ru
Добавить комментарий