Bab 3: Menulis Markup dengan JSX
⏱ 6 menit bacaApa Sih JSX Itu?
Coba bayangin kamu lagi bikin pesanan di warung. Kamu bilang ke abangnya: "Bang, nasi goreng satu, telor mata sapi, es teh manis." Kamu ngomong dalam satu kalimat yang jelas, terstruktur, dan abangnya langsung ngerti mau bikin apa.
JSX itu kayak "bahasa pesanan" buat React. Kamu nulis tampilan (UI) dalam format yang mirip HTML, tapi sebenernya itu JavaScript. React butuh tahu kamu mau nampilin apa di layar, dan JSX adalah cara kamu "memesan" tampilan itu.
// Ini JSX - mirip HTML tapi di dalam file JavaScript!
function Salam() {
return <h1>Halo, Dunia!</h1>;
}Sebelum JSX ada, developer harus nulis kayak gini:
// Tanpa JSX - ribet dan susah dibaca
function Salam() {
return React.createElement('h1', null, 'Halo, Dunia!');
}Kebayang kan bedanya? Yang pertama langsung keliatan "oh ini heading yang isinya Halo Dunia." Yang kedua? Kamu harus mikir dulu baru ngerti.
Kenapa React Pakai JSX?
Pertanyaan bagus. Kenapa nggak pakai HTML biasa aja?
Jawabannya: karena di React, logika dan tampilan itu satu paket.
Analoginya gini. Bayangin kamu punya warung makan. Di warung tradisional, ada pemisahan jelas: yang masak di dapur, yang nyajiin di depan. Tapi di warung modern (kayak food truck), yang masak dan yang nyajiin itu orang yang sama. Lebih efisien, lebih cepet, nggak ada miskomunikasi.
React itu kayak food truck. Komponen React ngurusin semuanya: logika (kapan tampil, data apa yang dipakai) DAN tampilan (gimana bentuknya di layar). JSX bikin kedua hal ini bisa ditulis bareng dalam satu tempat.
// Logika DAN tampilan jadi satu
function KartuProduk({ nama, harga, stok }) {
// Logika: cek stok
const tersedia = stok > 0;
// Tampilan: render berdasarkan logika
return (
<div>
<h2>{nama}</h2>
<p>Harga: Rp {harga.toLocaleString()}</p>
<p>{tersedia ? '✅ Tersedia' : '❌ Habis'}</p>
</div>
);
}JSX Bukan HTML!
Ini penting banget. Banyak pemula yang ngira JSX = HTML. Salah. JSX itu ekstensi sintaks JavaScript yang keliatan mirip HTML, tapi punya aturan sendiri.
Analoginya: Bahasa Indonesia dan Bahasa Melayu mirip banget. Tapi "budak" di Melayu artinya "anak", sedangkan di Indonesia... beda banget artinya. Mirip bukan berarti sama.
Berikut perbedaan utama JSX vs HTML:
| HTML | JSX | Kenapa? |
|---|---|---|
class="box" | className="box" | class itu kata kunci di JavaScript |
for="email" | htmlFor="email" | for itu kata kunci di JavaScript (loop) |
<br> | <br /> | Semua tag harus ditutup |
<img src="..."> | <img src="..." /> | Sama, harus self-closing |
tabindex="0" | tabIndex="0" | Atribut pakai camelCase |
onclick="..." | onClick={...} | Event handler pakai camelCase + kurung kurawal |
style="color: red" | style={{ color: 'red' }} | Style pakai objek JavaScript |
Aturan-Aturan JSX
Aturan 1: Harus Punya Satu Elemen Pembungkus (Single Root)
Setiap komponen React cuma boleh return satu elemen di level paling atas. Nggak boleh return dua elemen sejajar tanpa pembungkus.
// ❌ SALAH - dua elemen sejajar tanpa pembungkus
function Profil() {
return (
<h1>Budi Santoso</h1>
<p>Web Developer</p>
);
}// ✅ BENAR - dibungkus dalam satu div
function Profil() {
return (
<div>
<h1>Budi Santoso</h1>
<p>Web Developer</p>
</div>
);
}Kenapa harus satu root?
Bayangin kamu mau kirim paket lewat ojol. Kamu nggak bisa kasih dua kardus terpisah ke driver dan bilang "ini satu pesanan ya." Driver butuh satu paket yang jelas. Kalau mau kirim banyak barang, masukin ke satu tas/kardus besar dulu.
React juga gitu. Di balik layar, JSX diubah jadi objek JavaScript. Satu fungsi cuma bisa return satu nilai. Makanya kamu butuh satu "wadah" untuk membungkus semuanya.
Aturan 2: Semua Tag Harus Ditutup
Di HTML, beberapa tag boleh nggak ditutup (kayak <br>, <img>, <input>). Di JSX? Semua harus ditutup. Titik.
// ❌ SALAH - tag nggak ditutup
function FormLogin() {
return (
<div>
<input type="text">
<br>
<img src="logo.png">
</div>
);
}// ✅ BENAR - semua tag ditutup dengan />
function FormLogin() {
return (
<div>
<input type="text" />
<br />
<img src="logo.png" />
</div>
);
}Kenapa? Karena JSX itu lebih strict dari HTML. Parser JSX butuh tahu persis di mana setiap elemen mulai dan berakhir. Ini sebenernya hal bagus, karena mencegah bug yang susah dilacak.
Aturan 3: Atribut Pakai camelCase
Hampir semua atribut HTML yang terdiri dari dua kata atau lebih harus ditulis dalam format camelCase di JSX.
// HTML biasa
// <div class="container" tabindex="0" onclick="handleClick()">
// JSX - perhatikan perubahannya
<div className="container" tabIndex="0" onClick={handleClick}>Daftar konversi yang sering dipakai:
// HTML → JSX
// class → className
// for → htmlFor
// tabindex → tabIndex
// onclick → onClick
// onchange → onChange
// onsubmit → onSubmit
// maxlength → maxLength
// readonly → readOnly
// colspan → colSpan
// rowspan → rowSpanPengecualian: aria-* dan data-* tetap pakai tanda hubung (dash), nggak diubah ke camelCase.
// aria dan data tetap pakai dash
<button aria-label="Tutup" data-testid="btn-close">
✕
</button>Fragment: Pembungkus Tanpa Jejak
Kadang kamu butuh pembungkus tapi nggak mau nambah <div> yang nggak perlu di DOM. Solusinya: Fragment.
// Pakai div - nambah elemen yang nggak perlu
function InfoPengguna() {
return (
<div> {/* div ini cuma buat bungkus, nggak ada gunanya di tampilan */}
<h1>Siti Rahayu</h1>
<p>Jakarta, Indonesia</p>
</div>
);
}// Pakai Fragment - bersih, nggak ada elemen tambahan
function InfoPengguna() {
return (
<>
<h1>Siti Rahayu</h1>
<p>Jakarta, Indonesia</p>
</>
);
}<>...</> itu shorthand dari <React.Fragment>...</React.Fragment>. Keduanya sama aja, tapi yang pendek lebih sering dipakai.
Kapan pakai Fragment vs div?
- Pakai
<div>kalau kamu emang butuh wrapper untuk styling (kasih className, dll) - Pakai
<>kalau cuma butuh pembungkus supaya JSX valid, tanpa perlu elemen tambahan di DOM
// Contoh: Fragment berguna di tabel
function BarisData() {
return (
<>
<td>Nasi Goreng</td>
<td>Rp 15.000</td>
<td>Tersedia</td>
</>
);
}
// Kalau pakai div, tabel jadi rusak karena <div> di dalam <tr> itu invalidCatatan: Kalau kamu butuh kasih key (nanti dibahas di bab rendering list), pakai versi panjang:
{items.map(item => (
<React.Fragment key={item.id}>
<dt>{item.judul}</dt>
<dd>{item.deskripsi}</dd>
</React.Fragment>
))}Ekspresi JavaScript dalam JSX
Salah satu kekuatan JSX adalah kamu bisa nyisipin JavaScript di dalamnya pakai kurung kurawal {}.
function Sapaan() {
const nama = "Ahmad";
const jam = new Date().getHours();
return (
<div>
{/* Variabel */}
<h1>Halo, {nama}!</h1>
{/* Ekspresi/kalkulasi */}
<p>Sekarang jam {jam}:00</p>
{/* Ternary operator */}
<p>{jam < 12 ? 'Selamat Pagi' : 'Selamat Siang'}</p>
{/* Pemanggilan fungsi */}
<p>Nama uppercase: {nama.toUpperCase()}</p>
</div>
);
}Kita bahas lebih detail soal kurung kurawal di bab selanjutnya. Untuk sekarang, ingat aja: kurung kurawal {} = "ini JavaScript, bukan teks biasa."
Konversi HTML ke JSX: Panduan Praktis
Sering banget kamu punya HTML yang udah jadi (dari template, dari desainer, dari internet) dan perlu diubah ke JSX. Ini langkah-langkahnya:
Sebelum (HTML):
<div class="card">
<img src="foto.jpg" alt="Foto profil">
<h2 class="card-title">Nama Lengkap</h2>
<p tabindex="0">Deskripsi singkat</p>
<label for="email">Email:</label>
<input type="email" id="email" readonly>
<br>
<button onclick="handleClick()">Klik Saya</button>
</div>Sesudah (JSX):
function KartuProfil() {
return (
<div className="card">
<img src="foto.jpg" alt="Foto profil" />
<h2 className="card-title">Nama Lengkap</h2>
<p tabIndex="0">Deskripsi singkat</p>
<label htmlFor="email">Email:</label>
<input type="email" id="email" readOnly />
<br />
<button onClick={handleClick}>Klik Saya</button>
</div>
);
}Checklist Konversi:
- ✅
class→className - ✅
for→htmlFor - ✅ Semua self-closing tag dikasih
/> - ✅ Atribut multi-kata → camelCase
- ✅ Event handler → camelCase + kurung kurawal (bukan string)
- ✅
readonly→readOnly - ✅ Bungkus semuanya dalam satu root element
JSX Multi-baris dan Tanda Kurung
Kalau JSX kamu lebih dari satu baris, bungkus pakai tanda kurung (). Ini bukan aturan wajib secara teknis, tapi tanpa kurung, JavaScript bisa salah paham karena automatic semicolon insertion.
// ❌ Bisa bermasalah - return dan JSX di baris berbeda tanpa kurung
function Komponen() {
return
<div>Halo</div>;
// JavaScript nganggep ini: return; (undefined!)
// <div>Halo</div> nggak pernah dieksekusi
}// ✅ Aman - pakai kurung
function Komponen() {
return (
<div>
<h1>Halo</h1>
<p>Ini aman</p>
</div>
);
}// ✅ Juga aman - satu baris, nggak perlu kurung
function Komponen() {
return <h1>Halo</h1>;
}Aturan praktis: Kalau JSX kamu lebih dari satu baris, selalu pakai kurung () setelah return.
Komentar di JSX
Komentar di JSX sedikit beda dari HTML:
function Contoh() {
return (
<div>
{/* Ini komentar di dalam JSX */}
<h1>Judul</h1>
{/*
Komentar multi-baris
juga bisa kayak gini
*/}
<p>Paragraf</p>
</div>
);
}Perhatiin: komentar di dalam JSX harus dibungkus {/* ... */}. Komentar HTML biasa (<!-- -->) nggak akan jalan di JSX.
Di luar JSX (di bagian JavaScript biasa), kamu tetap bisa pakai // atau /* */ seperti biasa:
function Contoh() {
// Ini komentar JavaScript biasa - di luar JSX
const nama = "Budi"; /* ini juga bisa */
return (
<div>
{/* Ini komentar di dalam JSX */}
<h1>{nama}</h1>
</div>
);
}Contoh Lengkap: Kartu Profil
Mari kita gabungin semua yang udah dipelajari:
// Komponen KartuProfil - menampilkan info seseorang
function KartuProfil() {
// Data (nanti bisa dari props atau API)
const nama = "Dewi Lestari";
const pekerjaan = "Novelis & Musisi";
const kota = "Bandung";
const umur = 45;
const fotoUrl = "https://example.com/dewi.jpg";
return (
// Satu root element
<div className="kartu-profil">
{/* Bagian foto */}
<img
src={fotoUrl}
alt={`Foto ${nama}`}
className="foto-profil"
/>
{/* Bagian info */}
<div className="info">
<h2 className="nama">{nama}</h2>
<p className="pekerjaan">{pekerjaan}</p>
<p className="lokasi">📍 {kota}</p>
<p className="umur">{umur} tahun</p>
</div>
{/* Tombol aksi */}
<button
className="btn-follow"
onClick={() => alert(`Kamu follow ${nama}!`)}
>
Follow
</button>
</div>
);
}Kapan JSX Diubah Jadi Apa?
Di balik layar, setiap elemen JSX diubah oleh compiler (biasanya Babel atau SWC) jadi pemanggilan fungsi React.createElement():
// Kamu nulis ini:
<h1 className="judul">Halo Dunia</h1>
// Compiler ubah jadi ini:
React.createElement('h1', { className: 'judul' }, 'Halo Dunia')
// Yang menghasilkan objek seperti ini:
{
type: 'h1',
props: {
className: 'judul',
children: 'Halo Dunia'
}
}Kamu nggak perlu nulis React.createElement sendiri (makanya JSX diciptain!), tapi bagus untuk tahu apa yang terjadi di balik layar. Ini juga menjelaskan kenapa JSX punya aturan-aturan tertentu, karena pada akhirnya semua harus bisa diubah jadi pemanggilan fungsi JavaScript yang valid.
⚠️ Jebakan
Jebakan 1: Lupa Tutup Tag
// ❌ Error!
<img src="foto.jpg">
<input type="text">
// ✅ Benar
<img src="foto.jpg" />
<input type="text" />Pesan error yang muncul: Unterminated JSX contents atau Expected corresponding JSX closing tag
Jebakan 2: Pakai class Bukan className
// ❌ Ini nggak error tapi React kasih warning di console
<div class="container">Halo</div>
// ✅ Benar
<div className="container">Halo</div>Tips: Kalau kamu lihat warning "Invalid DOM property class" di console browser, ini penyebabnya.
Jebakan 3: Return Tanpa Kurung di Multi-baris
// ❌ Bug tersembunyi - return undefined!
function App() {
return
<div>
<h1>Halo</h1>
</div>;
}
// ✅ Benar - pakai kurung
function App() {
return (
<div>
<h1>Halo</h1>
</div>
);
}Jebakan 4: Dua Root Element
// ❌ Error!
function App() {
return (
<h1>Judul</h1>
<p>Paragraf</p>
);
}
// ✅ Benar - bungkus pakai Fragment
function App() {
return (
<>
<h1>Judul</h1>
<p>Paragraf</p>
</>
);
}Jebakan 5: Style Pakai String
// ❌ Salah - style bukan string di JSX
<div style="color: red; font-size: 20px">Halo</div>
// ✅ Benar - style pakai objek
<div style={{ color: 'red', fontSize: '20px' }}>Halo</div>Jebakan 6: Pakai for di Label
// ❌ Warning - for itu keyword JavaScript
<label for="username">Username:</label>
// ✅ Benar
<label htmlFor="username">Username:</label>Ringkasan
| Konsep | Penjelasan |
|---|---|
| JSX | Sintaks mirip HTML yang ditulis di JavaScript |
| Single Root | Setiap return harus punya satu elemen pembungkus |
| Self-closing | Semua tag harus ditutup (<br />, <img />) |
| camelCase | Atribut multi-kata pakai camelCase (className, onClick) |
| Fragment | <>...</> untuk bungkus tanpa elemen DOM tambahan |
Kurung () | Bungkus JSX multi-baris setelah return |
Kurung {} | Sisipkan ekspresi JavaScript di dalam JSX |
🏋️ Challenge
Challenge 1: Konversi HTML ke JSX
Konversi HTML berikut ke JSX yang valid:
<div class="recipe-card">
<img src="nasi-goreng.jpg" alt="Nasi Goreng">
<h2 class="recipe-title">Nasi Goreng Spesial</h2>
<ul>
<li tabindex="0">Nasi putih 2 piring</li>
<li tabindex="0">Kecap manis 2 sdm</li>
<li tabindex="0">Bawang merah 5 siung</li>
</ul>
<label for="porsi">Jumlah porsi:</label>
<input type="number" id="porsi" value="1" readonly>
<br>
<button class="btn-masak" onclick="mulaiMasak()">Mulai Masak!</button>
</div>💡 Hint
Ingat checklist konversi:
class→classNamefor→htmlFortabindex→tabIndexreadonly→readOnly- Self-closing tags perlu
/> - Event handler pakai camelCase + kurung kurawal
✅ Solusi
function KartuResep() {
return (
<div className="recipe-card">
<img src="nasi-goreng.jpg" alt="Nasi Goreng" />
<h2 className="recipe-title">Nasi Goreng Spesial</h2>
<ul>
<li tabIndex="0">Nasi putih 2 piring</li>
<li tabIndex="0">Kecap manis 2 sdm</li>
<li tabIndex="0">Bawang merah 5 siung</li>
</ul>
<label htmlFor="porsi">Jumlah porsi:</label>
<input type="number" id="porsi" value="1" readOnly />
<br />
<button className="btn-masak" onClick={mulaiMasak}>Mulai Masak!</button>
</div>
);
}Challenge 2: Bikin Komponen dengan Fragment
Buat komponen HeaderHalaman yang me-return judul (<h1>) dan deskripsi (<p>) tanpa elemen wrapper tambahan di DOM. Gunakan variabel untuk judul dan deskripsi.
💡 Hint
Pakai Fragment (<>...</>) supaya nggak ada <div> tambahan. Sisipkan variabel pakai kurung kurawal {}.
✅ Solusi
function HeaderHalaman() {
const judul = "Selamat Datang di Toko Online Kami";
const deskripsi = "Temukan berbagai produk berkualitas dengan harga terjangkau";
return (
<>
<h1>{judul}</h1>
<p>{deskripsi}</p>
</>
);
}Challenge 3: Kartu Produk Lengkap
Buat komponen KartuProduk yang menampilkan:
- Gambar produk (self-closing img)
- Nama produk (h3)
- Harga dalam format Rupiah
- Badge "DISKON" kalau ada diskon (pakai ternary)
- Tombol "Beli Sekarang"
Semua data simpan di variabel. Pastikan semua aturan JSX terpenuhi.
💡 Hint
- Pakai
toLocaleString('id-ID')untuk format angka Rupiah - Ternary operator:
{kondisi ? <tampilIni /> : null} - Jangan lupa className, self-closing tags, dan satu root element
✅ Solusi
function KartuProduk() {
const nama = "Sepatu Sneakers Lokal";
const harga = 450000;
const diskon = true;
const hargaDiskon = 360000;
const gambarUrl = "https://example.com/sepatu.jpg";
return (
<div className="kartu-produk">
<img
src={gambarUrl}
alt={nama}
className="gambar-produk"
/>
<div className="info-produk">
<h3 className="nama-produk">{nama}</h3>
{diskon ? (
<>
<p className="harga-coret">Rp {harga.toLocaleString('id-ID')}</p>
<p className="harga-diskon">Rp {hargaDiskon.toLocaleString('id-ID')}</p>
<span className="badge-diskon">DISKON</span>
</>
) : (
<p className="harga">Rp {harga.toLocaleString('id-ID')}</p>
)}
<button
className="btn-beli"
onClick={() => alert(`${nama} ditambahkan ke keranjang!`)}
>
Beli Sekarang
</button>
</div>
</div>
);
}Sudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.