Membuat Compound Components di Reactjs
Tags: react, pattern, frontend, indonesia
Published: 2022-05-09 06:34:52

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](https://mui.com/).

_*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:
```bash
yarn create vite modal-compound --template react
```

Setelah inisialiasi kita buka foldernya dan install dependenciesnya:
```bash
cd modal-compound && yarn install
```

Jika sudah di install jalankan dev servernya:
```bash
yarn dev
```

Install dependencies yang dibutuhkan:
```bash
yarn add @mui/material @emotion/react @emotion/styled react-nanny
```

`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`
```jsx
import { Button } from "@mui/material";

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

export default App;
```
`main.jsx`
```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>
);
```
Inisialisasi sudah selesai kini kita bermain di komponen modalnya. Coba membuat file di lokasi `src/components/modal/index.jsx` yang berisi:

```jsx
const ModalBody = () => {}

const ModalFooter = () => {}

const Modal = () => {}

export default Modal
```

Setiap komponen sudah dibuat waktunya menambahkan bentuk Namespace menjadi:

```jsx
const ModalBody = () => {}

const ModalFooter = () => {}

const Modal = () => {}

Modal.Body = ModalBody

Modal.Footer = ModalFooter

export default Modal
```

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

```jsx
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;
```

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:
```jsx
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;
```

Kini komponen `<Modal />` akan menerima props `onClose` dan `title`. Kita lanjut ke komponen `App.jsx` nya:
```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;
```

Hasilnya seperti ini:
<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9bti66vpw5ou4bonmjl6.gif" alt="Modal dasar">
</figure>

Waktunya penerapan Compound Component 😄 kini saya akan menggunakan utilitas [`react-nanny`](https://github.com/TheSpicyMeatball/react-nanny) untuk mencari komponen didalam children
```jsx
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;
```
Pada kode ini:
```jsx
const body = getChildByType(children, ModalBody);
const footer = getChildByType(children, ModalFooter);
```
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`:
```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;
```

Hasilnya:
<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/blsytcuas3xh5uipznhn.gif" alt="Modal dengan compound component">
  <figcaption>Maaf kalau desainnya kurang pas, nanti kita perbaiki hehe</figcaption>
</figure>

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

Modal.Footer = ModalFooter;
```

Oleh karena itu `Modal.Body` bisa panggil

Kita coba styling sedikit yaa:
`modal/index.jsx`
```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;
```
`App.jsx`
```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;
```

Hasil nya:
<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y2agev5gvum2wnrh34pt.gif" alt="Penambahan style">
  <figcaption>Lumayanlah yaa</figcaption>
</figure>

### Keuntungan ✨
Apa yaa keuntungan Compound Component ini? sepertinya sama aja mengguankan `children` biasa. Keuntungannya tuh disini:
```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.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;
```
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:
<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eeh4gogk1w8cdoqanx2b.gif" alt="Perubahan posisi">
</figure>
 
### 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