DEV Community

alfianandinugraha
alfianandinugraha

Posted on • Updated on

Membuat Compound Components di Reactjs

Selamat datang di post perdana saya tentang Reactjs 😄 Kali ini saya coba bahas salah satu pattern di Reactjs yang paling banyak digunakan yaitu Compound Components.

Compound dalam bahasa Indonesia artinya menggabungkan. Jadi Compound Components merupakan menggabungkan berapa komponen menjadi satu komponen.

Lhooo kan emang seperti itu kalau komponen di Reactjs 🤔

Nahh bedanya, kalau Compound Component ini komponen nya hanya bisa digunakan dalam scope tertentu saja. Kita contohkan pada HTML biasa. Di HTML terdapat tag <table />, <tbody />, dan <thead />. Tag <tbody /> dan <thead /> ini adalah bagian dari <table /> dan tidak bisa di gunakan diluar <table /> (bisa sih, tapi tidak ada efeknya).

Di komponen Reactjs juga bisa dibuat seperti itu lhoo 😁 Sekarang kita coba bikin studi kasus mengenai komponen Modal.

Pertama kita rancang dulu bagian-bagian pada Modal, yaitu:

  • Wrapper
  • Body
  • Footer

Ada 3 bagian utama pada Modal, maka bisa kita buat komponen setiap bagiannya dengan nama:

  • <Modal />, untuk Wrapper
  • <Modal.Body />, untuk Body
  • <Modal.Footer />, untuk Footer

*FYI: Bentuk komponen diatas disebut Namespace Component

Perancangan kita selesai, kini waktunya programming. Pertama-tama saya akan menggunakan Vite + React, kalau kalian pake create-react-app juga tidak masalah dan saya juga menggunakan UI framework yang bernama Material UI.

*Note: kamu tidak perlu berpatok dengan apa yang saya gunakan, kamu bisa menggunakan CRA dengan React-bootstrap serta NPM

Pertama kita inisialisasi proyeknya terlebih dahulu menggunakan vite:

yarn create vite modal-compound --template react
Enter fullscreen mode Exit fullscreen mode

Setelah inisialiasi kita buka foldernya dan install dependenciesnya:

cd modal-compound && yarn install
Enter fullscreen mode Exit fullscreen mode

Jika sudah di install jalankan dev servernya:

yarn dev
Enter fullscreen mode Exit fullscreen mode

Install dependencies yang dibutuhkan:

yarn add @mui/material @emotion/react @emotion/styled react-nanny
Enter fullscreen mode Exit fullscreen mode

react-nanny? apaan tuh? itu merupakan utilitas tambahan untuk mencari child dari react children. Mirip seperti slot pada Vue

Kalau sudah di install kini inisialisasi App.jsx dan main.jsx terlebih dahulu:
App.jsx

import { Button } from "@mui/material";

function App() {
  return (
    <div>
      <Button variant="contained">Open Modal</Button>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

Inisialisasi sudah selesai kini kita bermain di komponen modalnya. Coba membuat file di lokasi src/components/modal/index.jsx yang berisi:

const ModalBody = () => {}

const ModalFooter = () => {}

const Modal = () => {}

export default Modal
Enter fullscreen mode Exit fullscreen mode

Setiap komponen sudah dibuat waktunya menambahkan bentuk Namespace menjadi:

const ModalBody = () => {}

const ModalFooter = () => {}

const Modal = () => {}

Modal.Body = ModalBody

Modal.Footer = ModalFooter

export default Modal
Enter fullscreen mode Exit fullscreen mode

Sekarang kita menambahkan prop children disetiap bagian modal nya. Menjadi:

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;
Enter fullscreen mode Exit fullscreen mode

Pada komponen <Modal /> diatas saya menggunakan react-portal agar bisa di render di element dengan id root

Sekarang kita coba styling simple aja untuk komponen <Modal /> ini:

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;
Enter fullscreen mode Exit fullscreen mode

Kini komponen <Modal /> akan menerima props onClose dan title. Kita lanjut ke komponen App.jsx nya:

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;
Enter fullscreen mode Exit fullscreen mode

Hasilnya seperti ini:

Modal dasar

Waktunya penerapan Compound Component 😄 kini saya akan menggunakan utilitas react-nanny untuk mencari komponen didalam children

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;
Enter fullscreen mode Exit fullscreen mode

Pada kode ini:

const body = getChildByType(children, ModalBody);
const footer = getChildByType(children, ModalFooter);
Enter fullscreen mode Exit fullscreen mode

Digunakan untuk mencari komponen dengan komponen dasarnya. Misalkan getChildByType(children, ModalBody) ini berarti saya mencari komponen ModalBody didalam children.

Karena children ini bisa menerima banyak sekali komponen. Oleh karena itu kita memilih komponen yang dibutuhkan saja. Inilah Compound Components.

Penggunaannya yaitu pada 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;
Enter fullscreen mode Exit fullscreen mode

Hasilnya:

Modal dengan compound component


Maaf kalau desainnya kurang pas, nanti kita perbaiki hehe

Sebentar 🤔 Kok bisa ModalBody bisa kepilih padahal kita menggunakan Modal.Body bukan ModalBody. Nahh ingat, pada komponen <Modal /> kita sudah membuat ini:

Modal.Body = ModalBody;

Modal.Footer = ModalFooter;
Enter fullscreen mode Exit fullscreen mode

Oleh karena itu Modal.Body bisa panggil

Kita coba styling sedikit yaa:
modal/index.jsx

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Hasil nya:

Penambahan style


Lumayanlah yaa

Keuntungan ✨

Apa yaa keuntungan Compound Component ini? sepertinya sama aja mengguankan children biasa. Keuntungannya tuh disini:

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;
Enter fullscreen mode Exit fullscreen mode

Kamu bisa mengisi <Modal.Footer /> terlebih dahulu lalu <Modal.Body />. Kalau menggunakan children biasa, sudah pasti posisinya berubah. Nahh kalau menggunakan Compound Component ini meski posisi di parent nya berubah, tapi didalam komponen Compound nya tidak akan berubah

Hasilnya:

Perubahan posisi

Kekurangan 🔻

Sejauh pengalaman saya, kekurangan dari Compound Components ini adalah setup komponen yang lama. Kita harus mendefinisikan setiap bagiannya (Header, Body, dll). Jadi tetap ada kekurangannya hehe

Penutup

Mungkin itu saja pembahasan mengenai Compound Component pada Reactjs. Kalau menurutmu ini bermanfaat silahkan bagikan ke teman-teman mu yaa 😄

Sampai jumpa di tutorial React selanjutnya 👋

Oh iyaa untuk source code nya kunjungi https://github.com/alfianandinugraha/modal-compound

Top comments (0)