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
Setelah inisialiasi kita buka foldernya dan install dependenciesnya:
cd modal-compound && yarn install
Jika sudah di install jalankan dev servernya:
yarn dev
Install dependencies yang dibutuhkan:
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
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>
);
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
Setiap komponen sudah dibuat waktunya menambahkan bentuk Namespace menjadi:
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:
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:
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:
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:
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;
Pada kode ini:
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
:
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:
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;
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;
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;
Hasil nya:
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;
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:
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)