Episode ini membahas sisi tampilan Remix: membuat komponen React reusable, styling dengan CSS Modules, Tailwind, dan styled-components, interaktivitas client-side, serta cara mengoptimalkan UI yang di-render di server.

Routing dan data sudah kalian kuasai. Sekarang masuk ke bagian yang paling terlihat: UI dan komposisi komponen. Di sinilah kode teknis berubah menjadi pengalaman yang dirasakan pengguna.
Remix adalah React, jadi seluruh kebiasaan komponen React berlaku. Yang membedakan adalah disiplin: karena komponen di-render di server lalu di-hydrate di client, kalian harus sadar kode mana yang jalan di server, di client, atau keduanya. Episode ini mengajarkan kebiasaan itu sejak awal.
Episode 6 akan membahas komponen reusable, tiga pendekatan styling yang umum di ekosistem Remix, interaktivitas client-side, dan cara mengoptimalkan UI yang memang dilahirkan di server.
Komponen adalah fungsi yang menerima props dan mengembalikan elemen UI. Kunci reuse adalah memisahkan bagian yang berulang menjadi komponen kecil dengan tanggung jawab tunggal — misalnya tombol, kartu, atau badge — lalu mengomposisikannya di halaman.
export function Kartu({ judul, children }) {
return (
<article>
<h2>{judul}</h2>
<div>{children}</div>
</article>
);
}Props menentukan perilaku komponen; children memungkinkan komposisi bersarang. Dengan pola ini, halaman hanya menyusun blok, bukan mengulang markup yang sama berkali-kali.
Simpan komponen bersama route yang memakainya, atau di folder app/components bila dipakai lintas halaman. Pola ini akan dibahas lebih dalam di episode 18. Untuk sekarang, cukup ingat prinsipnya: satu komponen, satu file, satu tanggung jawab.
Remix mendukung CSS Modules tanpa konfigurasi tambahan. Buat file .module.css, import di komponen, dan Remix menangani scope-nya. Kelas di-scope per komponen sehingga tidak ada tabrakan nama antar file.
.tombol { background: #2563eb; color: white; padding: 8px 16px; }import styles from "./Button.module.css";
export function Tombol() {
return <button className={styles.tombol}>Klik</button>;
}Import styles dari .module.css menghasilkan objek yang memetakan nama kelas. Remix bahkan mengoptimalkan CSS dengan mengirim hanya file yang dipakai route tertentu.
Tailwind adalah pilihan paling populer di komunitas Remix. Integrasinya sederhana: install paket Tailwind, jalankan init, dan hubungkan path konten ke folder app. Khususnya di Remix v3 yang berbasis Vite, plugin Tailwind bisa dipasang langsung di vite.config.ts.
styled-components dan CSS-in-JS lain juga berfungsi, tetapi ada biaya: CSS baru di-inject setelah hydration, sehingga bisa menyebabkan flash. Jika kalian memilih CSS-in-JS, pertimbangkan pendekatan yang mendukung rendering di server. Untuk aplikasi berbasis konten, CSS Modules dan Tailwind umumnya lebih cocok.
Meski di-render di server, komponen tetap React penuh setelah hydration. Event handler seperti onClick dan hooks state seperti useState berjalan normal di client. Interaktivitas yang murni lokal — toggle, tab, accordion — tidak perlu loader atau action.
import { useState } from "react";
export default function Akordeon({ judul, isi }) {
const [terbuka, setTerbuka] = useState(false);
return (
<div>
<button onClick={() => setTerbuka(!terbuka)}>{judul}</button>
{terbuka ? <p>{isi}</p> : null}
</div>
);
}State lokal seperti useState hanya ada di client setelah hydration. UI yang butuh data server tetap memakai loader; interaksi murni visual cukup memakai state lokal.
Gunakan client rendering untuk bagian yang membutuhkan waktu nyata: chat, timer, drag and drop. Selebihnya, biarkan server yang me-render. Prinsip ini menjaga halaman tetap cepat karena JavaScript yang dikirim hanya untuk interaksi yang benar-benar ada.
Karena Remix mengirim HTML yang sudah berisi konten, first paint sangat cepat. Optimasi utama berikutnya adalah mengirim sesedikit mungkin JavaScript. Hindari menulis fetch manual di komponen — itu tugas loader. Gunakan Link prefetch untuk menyiapkan data halaman berikutnya saat hover.
import { Link } from "@remix-run/react";
export default function DaftarArtikel() {
return <Link prefetch="intent" to="/artikel/1">Baca artikel</Link>;
}Prop prefetch="intent" meminta Remix me-load data route saat kursor mendekati link. Klik terasa instan karena data sudah siap di cache client.
Terakhir, biasakan memeriksa hasil kerja dengan Lighthouse di browser. Ukur waktu sampai konten pertama terlihat, jumlah JavaScript yang dikirim, dan skor aksesibilitas. Episode 9 akan membahas optimasi performa lebih dalam, termasuk caching.
Episode 6 menutup segitiga UI: komponen reusable yang diorganisir rapi, styling dengan CSS Modules, Tailwind, atau styled-components, interaktivitas client-side yang sadar hydration, dan optimasi agar UI server-rendered tetap ringan di client.
Inti yang harus dibawa pulang:
Di episode 7 selanjutnya kita akan membahas forms dan validation — form handling dengan komponen Form dari Remix, validasi di sisi server dengan feedback di client, rendering error per field, dan progressive enhancement agar form tetap berfungsi tanpa JavaScript. Form adalah tempat Remix benar-benar bersinar.