Panduan Jadi Mualaf React Router v7 (formerly Remix)

Di tulisan ini banyak opini pribadi gue. Jadi, hati-hati, ya.

Ada banyak hal yang bisa dijadiin alesan buat nggak pake Next.js, mulai dari butuh resource intensif buat dev server, versi stable yang nggak stable, atau bisa juga alasan moral. Ironisnya, web ini juga dibikin pake Next.js~

Dulu, Next.js seolah kayak mesias buat gue yang males setup masalah routing di React. Sampe kebabalsan 5 tahun pake Next.js dan jadi buzzernya di semua platform yang gue punya.

Dari tahun lalu udah nyoba nyari alternatif yang cocok buat gue dan sempet juga nyoba Remix, tapi belum nemu alasan yang kuat buat bener-bener pindah. Nggak sekuat alesan gue dulu pake Vim. Alesan-alesan teknis ini gue rasa masih selalu bisa diakalin walaupun sambil love-hate sama Next.js. Tapi, ya gimana lagi belum nemu yang cocok saat itu.

Akhirnya beberapa waktu lalu gue nemu alesan yang kuat banget buat pindah dan jadi mualaf React Router. Saat itu juga gue langsung cari alternatif yang bener-bener bisa diandelin. Beberapa kandidatnya nggak banyak:

  • Tanstack Start
  • Sveltekit
  • Remix (sekarang React Router v7)

Kenapa Nggak Tanstack Start atau Sveltekit?

Ada beberapa alasan kenapa gue nggak pake Tanstack Start:

  • Nggak suka sama APIs-nya, sintaksisnya kayak bloated. Di satu sisi bagus, nggak ada magic yang disembunyiin, tapi di sisi lain terlalu ribet buat orang pragmatis kayak gue.
  • Masih baru banget. Ini nggak begitu masalah, karena gue suka nyobain teknologi baru, tapi, gue juga butuh solusi yang bener-bener bisa diandelin buat proyek gue berikutnya.
  • Sejujurnya dokumentasinya lebih bikin bingung dibanding Remix. Kalo dibilang deskriptif, iya, tapi nggak straight to the point.

Walaupun begitu, gue suka beberapa hal dari Tanstack Start, kayak server functions dan end-to-end type safety-nya.

Sementara Sveltekit, gue harus belajar Svelte dulu. Gue bakal gas ini kalo ini proyek kantor~

Kenapa Remix?

Selain karena gue udah pelajarin dasar-dasar Remix dari tahun lalu, gue juga betah pakenya. Ini yang bikin gue akhirnya mutusin buat pake Remix. Tapi, saat gue mau coba baca-baca lagi, ternyata Remix yang baru ini udah di-merge ke React Router v7.

Bedanya Remix dan React Router

Jadi, simpelnya, Remix ini dulunya tuh kayak "branch" dari React Router buat solusi full-stack pake React yang didasari React Router. Bener-bener rely heavily on React Router. Itu kenapa mereka bilangnya, Remix ini kayak "thin layer"-nya React Router. Karena emang sebagian besar itu fitur-fiturnya React Router.

Kemudian, mereka bikin banyak perubahan di Remix yang baru ini. Banyak orang ini bakal jadi Remix v3, termasuk gue. Tapi, ternyata perubahan-perubahan itu dijadiin React Router v7 dalam bentuk "framework mode". Jadi, fitur-fitur Remix yang udah ada dibawa ke React Router yang baru dan lo bisa pake itu hari ini.

React Router sekarang jadinya punya 3 mode:

  • Declarative: ini cuma buat router aja, yang mungkin biasa atau udah lo pake dari jauh-jauh hari.
  • Framework: ini yang baru di React Router v7, setara sama Next.js.
  • Data: ini lebih kayak versi mini dari framework, ada beberapa yang lo nggak dapet di mode ini, kayak <Link prefetech> atau meta contohnya.

Buat pengguna React Router v6, lo bisa upgrade ke v7 buat nyoba fitur-fitur baru dan khusunya di mode framework. Buat pengguna Remix v2, lo juga bisa "switch" ke React Router v7 buat dapet pengalaman yang harusnya lo dapetin di "Remix v3".

Remix ke depannya bakal tetep lanjut, beberapa waktu lalu Ryan Florence sama Michael Jackson nulis kalo Remix bakal tetep jadi solusi full-stack tapi pake teknologi yang bukan lagi React. Mereka bakal nge-fork Preact yang bakal dijadiin dasar framework-nya. Preact sendiri adalah alternatif React yang lebih cepet dengan API yang sama. Walaupun begitu, React Router bakal tetep dapet update karena udah ada timnya sendiri yang fokus di situ.

Kalo boleh gue ringkas, lo bisa pake React Router v7 mode framework buat full-stack yang basisnya React. Nanti, lo bisa nunggu Remix yang baru rilis kalo mau nyoba solusi yang beda di luar dari React. Intinya, dua-duanya bakal jalan bareng.

Karena gue datang dari Next.js, maka ada beberapa hal yang gue cari dan jadi alesan gue buat pindah ke React Router v7 (yang selanjutnya bakal gue sebut sebagai React Router):

Dev Server yang Lebih Ringan dan Cepet

Gue masih inget banget waktu masih pake Next.js, memory gue abis 3-4GB cuma buat jalain dev server doang, anjing. Klaim doang 10x faster bla-bla-bla, gue nyimpen perubahan aja nggak langsung reflect, musti nunggu beberapa detik dulu. Segitu gue pake Macbook M1.

Sementara di Remix, kalo gue bikin perubahan itu langsung reflek in no time alias instan banget. Memory juga nggak abis banyak, sejauh ini nggak pernah sampe lebih dari 1GB, tapi gue nggak tau kalo proyeknya lebih kompleks, let's see.

Ini sih yang jadi alesan yang signifikan gue suka sama React Router.

Routing yang Sentris

Gue dulu suka banget sama file-system routing-nya Next.js, karena bikin routing jadi gampang. Tinggal bikin file doang udah jadi route tanpa setup ini-itu. Tapi, ketika gue udah mulai bikin-bikin produk enterprise-nya Kredibel yang menurut gue cukup kompleks ini bikin gue sadar kalo file-system routing di Next.js ini nggak banget. Alesannya:

  • Semakin panjang path, semakin dalem juga struktur foldernya.
  • Kalo gue ganti path, gue pindahin lagi struktur foldernya.
  • Next.js makin banyak file convention, kayak route groups salah satunya. Ini yang kadang bikin gue bingung buat nyari lokasi file buat route yang gue cari.
  • Penamaan file yang sama ini yang bikin susah dicari dan bikin pusing, gue jadi harus semakin spesifik. Ngetik "page.tsx" doang kan nggak cukup, harus lebih spesifik lagi.

Sementara di React Router, mereka punya sistem routing yang sentris lewat file routes.ts. Kalo lo pernah pake Laravel, ini kayak web.php atau api.php lah, cuman di satu file yang sama. Kalo gini lebih bikin gampang. Misal, lo lagi nerusin kerjaan orang atau kerjaan lo sendiri setelah beberapa bulan dan lo lupa lokasi suatu route, lo tinggal buka aja file routes.ts. Di situ lo bisa tau kalo suatu route itu dia di-reference ke component yang mana. Gini contohnya:

app/routes.ts

Kalo mau ganti path? Tinggal ganti, nggak ada struktur folder yang diubah. Nyari file? nggak ada lagi tuh "page.tsx" yang bikin pusing. Struktur folder? Nggak harus ngikutin struktur path-nya. Dan juga nggak banyak file convention di dalam folder app, lo bisa naruh component atau apapun di folder tersebut tanpa khawatir terekspos ke publik atau perlu nandain pake simbol-simbol kayak _ atau ( ). Utter woke nonsense!

Selesai itu Next.js!

Data Loading

Kalo di Next.js disebutnya data fetching, di React Router pake istilah data loading. Pada dasarnya sama aja cuman beda istilah aja. Untuk data loading di React Router, kita bisa nge-export fungsi loader di dalem file yang disebut route module. Route module ini simpelnya file yang lo referensiin di file routes.ts. Kalo ada kode route("/home", "./home.tsx"), nah, file home.tsx itu yang disebut route module.

Contoh loader di React Router:

app/routes/product.tsx

Ini mirip sama getServerSideProps-nya Next.js versi pages router. Fungsi tersebut nantinya bakal dieksekusi di server ketika route tersebut di-render buat initial load. Fungsi ini juga bakal dihapus dari client bundle, jadi lo nggak perlu khawatir buat pake API yang cuman boleh jalan di server (contoh: database).

Alternatif dari loader ini ada lagi, yaitu clientLoader. Lo udah bisa nebak bedanya apa, kan? Iya, bedanya fungsi ini bakal dieksekusi di client/browser.

Actions

Action ini fitur React Router yang lo bisa pake buat handle form submission. Sama kayak loader, action juga ada dua variant: action yang dieksekusi di server; clientAction yang dieksekusi di client/browser. Enaknya action di React Router, kita nggak musti bikin file baru cuman buat naro function-nya, alih-alih kita bisa nge-export action di dalem satu file route module yang sama.

app/routes/project.tsx

Fungsi action juga bakal dihapus dari client bundle, jadi lo aman buat pake API yang cuman boleh jalan di server.

End-to-end Type Safety

End-to-end type safety dalam konteks ini simpelnya adalah lo bisa nulis kode di server dan client dengan tipe data yang sama. Lo nggak perlu khawatir dan capek-capek bikin tipe data yang sama di server dan client, React Router bakal ngurusin itu buat lo. Hal ini juga bisa lo di temuin di loader dan action.

Kalo lo liat contoh di atas, loader itu ngasih return value yang tipe datanya otomatis di-infer. Jadi, ketika lo pake loaderData di component, lo udah bisa dapet tipe data yang sesuai dengan apa yang lo return di loader.

app/routes/product.tsx

Ini keren, karena lo nggak perlu validasi manual di client atau server yang mungkin biasanya lo lakuin ketika pake fetch.

Fitur Lainnya

Selain fitur-fitur di atas, React Router juga punya beberapa fitur lain yang biasanya kita butuhin:

  • Streaming
  • Error boundary
  • Meta tags
  • Prefetching
  • Form component
  • Testing
  • Dan masih banyak lagi

Gue nggak bakal bahas semuanya di sini secara detail, beberapa yang umum aja yang bakal gue bahas.

Pembahasan

Gue bakal bahas beberapa fitur yang umum biasanya kita pake di Next.js, supaya lo bisa lebih mudah beradaptasi dengan React Router sebagai pengganti Next.js. Itu berarti gue bakal bahas React Router mode framework dengan SSR (Server-Side Rendering) enabled.

Instalasi

Buat mulai project baru React Router, lo bisa perintah berikut:

Terminal

Biasanya, lo bakal ditanya mau install dependencies sekalian atau nggak. Kalo lo nggak sengaja pilih "No", lo bisa install dependencies-nya manual pake perintah berikut:

Terminal

Buat jalanin dev server, lo bisa pake perintah berikut:

Terminal

Sekarang lo bisa akses aplikasi lo di http://localhost:5173.

Kedepannya, gue bakal asumsiin lo pake pnpm, ya. Karena gue pake pnpm di sini. Nggak beda jauh kok perintahnya~

Pengenalan File dan Folder

Setelah lo bikin project baru, lo bakal nemu struktur folder dan file yang udah ada secara bawaan. Beberapa file lo bisa liat secara langsung, tapi ada juga beberapa yang disembunyiin secara default, kayak entry.client.tsx dan entry.server.tsx contohnya. Ini karena emang file-file tersebut nggak perlu di-apa-apain, kecuali ada case khusus yang lo butuhin.

.react-router

Folder ini di-generate otomatis sama CLI-nya React Router. Isi dari folder ini adalah informasi kayak type yang di-generate otomatis sesuai konfigurasi di file app/routes.ts. Secara umum, lo nggak perlu ngurusin folder ini, tapi lo bisa liat-liat isinya kalo lo penasaran.

app

Folder ini bisa lo isi dengan kode aplikasi lo. Beda dengan Next.js, di dalem folder ini nggak banyak file convention dan ada beberapa file bawaan:

app/routes.tsx

File ini adalah file konfigurasi routing yang bisa lo sesuaikan dan file ini juga yang jadi sumber React Router buat nge-generate type untuk setiap route module (lo bisa baca lebih lanjut di bagian Routing).

app/root.tsx

File ini juga bisa disebut root route, dia yang bakal jadi parent buat semua route yang ada di aplikasi lo. Kalo di Next.js, ini mirip kayak root layout-nya mereka lah atau file app/layout.tsx. Ini kenapa apa yang lo declare di file ini, bakal "dibagi" ke semua route yang ada di aplikasi lo.

Di dalem file ini lo bakal nemu <Outlet />, yang fungsinya buat nge-render child route. Jadi, setiap route yang lo bikin di dalam file routes.tsx, bakal di-render di dalam <Outlet /> ini.

app/root.tsx

Juga lo bakal nemu export function Layout yang bisa lo pake buat nge-define layout component buat ngebungkus setiap route. Setiap kali user pindah route, layout ini bakal tetep ada dan nggak bakal di-render ulang. Karena file ini adalah root route, jadi lo harus define tag <html>, <head>, dan <body> di dalamnya. Mirip kayak app/layout.tsx di Next.js.

app/root.tsx

Lo bisa juga import global CSS kayak gini:

app/root.tsx

Bisa juga export component ErrorBoundary yang bakal dipake buat handle error di seluruh aplikasi lo (termasuk error 404). Kalo di Next.js, ini mirip app/global-error.tsx perannya.

app/root.tsx

Lo juga bisa export links buat nge-load CSS external kayak Google Fonts, misalnya:

app/root.tsx

File ini juga support beberapa fitur yang bisa dipake di route module, kayak loader dan action contohnya.

Colocation

Selain kedua file di atas, sebenernya ada folder lain dengan nama welcome yang ada di dalam folder app. Isi dari folder ini cuman satu file welcome.tsx dan dua file logo. Lo bisa hapus folder ini kalo lo mau, karena ini cuman contoh aja.

Lo bisa bikin folder lain di dalam folder app buat naro file-file yang lo butuhin, misal components, utils, atau hooks. Gue sendiri nge-treat folder app ini kayak folder src yang biasanya ada di project React lainnya. Tapi, ini preferensi pribadi aja, lo bisa naruh file-file lo di luar folder app kalo mau. Gue cuman ngasih tau aja kalo cara ini aman dan lo nggak perlu khawatir ada file yang namanya bakal bentrok sama convention yang ada di React Router. Soalnya ada framework lain yang bentar lagi mau mati ketiban convention-nya sendiri.

public

Folder public ini adalah tempat lo naro file-file statis yang bisa diakses langsung dari URL. Misalnya, lo mau naro gambar atau file aset yang bisa diakses langsung, lo bisa taro di sini.

vite.config.ts

React Router ini pake Vite sebagai bundler-nya, jadi lo bakal nemuin file konfigurasi Vite di sini. Lo bisa ubah konfigurasi Vite sesuai kebutuhan lo, misalnya nambahin plugin kayak Tailwind CSS atau apaun lah pokoknya. Nyatanya, Tailwind CSS ini udah di-setup secara bawaan, jadi lo tinggal pake aja.

react-router.config.ts

Ini adalah file konfigurasi React Router. Secara umum, lo nggak perlu ngubah apa-apa di sini, kecuali ada prilaku dari React Router yang mau lo ubah, kayak nge-disable SSR atau nge-enable feature experimental contohnya.

Routing

Secara bawaan, React Router punya sistem routing yang sentris, jadi lo bisa bikin routing di satu file routes.ts yang ada di folder app. Kalo lo buka file tersebut, lo bakal nemu satu route bawaan yang jadi halaman utama bawaan React Router:

app/routes.ts

Kalo lo mau bikin route baru, tinggal tambahin aja di array tersebut. Misal, lo mau bikin route /about, lo bisa tambahin kayak gini:

app/routes.ts

Fungsi route butuh dua parameter: path dan file module-nya yang bakal di-render ketika route tersebut match atau diakses.

Isi dari file route module-nya itu React component yang diekspor secara default. Contohnya kayak gini:

app/routes/about.tsx

Kalo lo liat lagi file routes.ts, di sana lo notice ada index dan route. index itu buat route yang jadi halaman utama dari parent route-nya, sedangkan route itu buat route biasa. Secara teknis kalo lo ganti index ke route, itu juga bakal jalan, tapi secara semantik lebih baik pake index kalo itu halaman utama dari parent route-nya.

Setiap file yang lo referensiin di routes.ts itu disebut route module. Route module ini bisa punya beberapa fungsi yang bisa lo export, kayak loader, action, dan meta. Kalo liat lagi kode di atas, file routes/home.tsx dan routes/about.tsx itu adalah route module.

Path import route module ini relatif ke file routes.ts, ini berarti dia mulai dari folder app. Jadi, ketika lo nulis routes/home.tsx, itu berarti lo ngimpor file home.tsx yang ada di dalam folder app/routes.

Di mana lo mau naruh route module itu terserah lo, karena nggak ada convention khusus buat itu. Cuman, biasanya orang naruh di folder routes atau pages biar gampang dicari dan lebih terstruktur.

Kayak gini nggak masalah:

app/routes.ts

Kalo kayak gini lo nggak perlu lagi bikin struktur folder yang dalem-dalem buat ngikutin struktur path-nya. Hal ini juga yang ngatasin masalah pribadi gue, dan akhirnya gue bisa bikin struktur folder yang sedangkal mungkin.

Nested

Anggap halaman /about itu punya subhalaman /about/team. Lo bisa bikin route-nya kayak gini:

app/routes.ts

Kalo lo akses /about/team, lo bakal liat tampilan yang sama kayak /about. Ini karena route /about dianggap sebagai parent dari route /about/team. Untuk itu, lo perlu nge-render component <Outlet /> di dalam component /about supaya subhalaman tersebut bisa ditampilin.

app/routes/about.tsx

Secara teknis, route module /about/team ini dibungkus di dalam route module /about. Kayak gini kira-kira:

Struktur Route

Ini berarti route module /about juga bisa kita dijadikan sebagai layout untuk subhalaman /about/team dan subhalaman lainnya yang ada di bawahnya. Ini bermanfaat banget ketika lo mau bikin semacam sub-menu berupa sidebar atau tab yang ada di dalam halaman /about.

Ini juga berarti, dalam konteks ini, lo nggak perlu naruh konten halaman /about di dalam file routes/about.tsx. File tersebut bisa dijadiin sebagai layout aja, dan lo bisa bikin file baru yang khusus buat konten halaman /about di dalam file app/routes/about-index.tsx, misalnya.

Jadinya kayak gini:

app/routes.ts

Untuk isi file app/routes/about.tsx, lo bisa bikin kayak gini:

app/routes/about.tsx

Sedangkan untuk file app/routes/about-index.tsx, lo bisa bikin kayak gini:

app/routes/about-index.tsx

Dan untuk file app/routes/about-team.tsx, lo bisa bikin kayak gini:

app/routes/about-team.tsx

In case lo nggak mau bikin nested route dengan struktur kayak gini, lo bisa bikin pake cara yang lebih simpel:

app/routes.ts

Kalo kayak gini, file app/routes/about.tsx nggak perlu pake <Outlet /> lagi, karena nggak ada subhalaman yang di-render di dalamnya dan kedua route tersebut nggak punya parent-child relationship. That's perfectly fine kalo lo butuhnya kayak gini.

If it works, it works!

Dynamic

Dynamic segments lo butuhin ketika lo perlu parameter di dalam path suatu route. Misal, lo bikin satu halaman buat detail produk dengan path /product/baju-pria-001, bagian baju-pria-001 itu yang jadi parameter dinamisnya. Di React Router, lo bisa bikin dynamic segments dengan cara nambahin : di depan nama parameter yang lo mau.

Contohnya:

app/routes.ts

Di dalem file app/routes/product.tsx, lo bisa akses parameter tersebut lewat params yang ada di dalam component props. Contohnya:

app/routes/product.tsx

Kalo di Next.js, lo harus nulis type manual buat parameter dinamisnya, misal { pid: string }. Sementara di React Router, lo bisa langsung akses params yang udah di-infer tipe datanya dari path yang lo tulis di routes.ts.

Liat lagi kode di atas, di baris pertama ada import type { Route } from './+types/product';. Ini adalah type yang di-generate otomatis oleh React Router buat route module ini. Setiap route module yang lo bikin, React Router bakal generate type-nya di dalam folder spesial .react-router/types/. Dengan konfigurasi rootDirs di tsconfig.json, lo bisa import type tersebut seolah-olah itu ada di dalam folder yang sama dengan route module-nya. Dengan demikian, lo bisa dapet type safety buat props yang ada di dalam component lo, termasuk params, loaderData, dan actionData.

Lo juga bisa bikin beberapa dynamic segments di dalam satu route. Misal, lo mau bikin halaman detail produk dengan kategori, lo bisa bikin kayak gini:

app/routes.ts

Di dalam file app/routes/product.tsx, lo bisa akses kedua parameter tersebut lewat params:

app/routes/product.tsx

Lo juga bisa bikin dynamic segments jadi opsional dengan cara nambahin ? di belakang nama parameter. Misal, lo mau bikin halaman detail produk yang bisa diakses dengan atau tanpa kategori, lo bisa bikin kayak gini:

app/routes.ts

Di dalam file app/routes/product.tsx, lo bisa akses parameter tersebut dengan cara yang sama seperti sebelumnya:

app/routes/product.tsx

Jadi, route tersebut bakal match dengan path /product/category/baju-pria-001 dan /product/baju-pria-001.

Prefix

Prefix ini fungsi yang bikin lo bisa bikin route path dengan awal yang sama. Misal, lo mau bikin route /admin/users dan /admin/products, lo bisa bikin kayak gini:

app/routes.ts

Karena prefix ini return-nya adalah array, lo bisa langsung spread ke dalam array route yang ada di routes.ts. Ini bikin lo bisa bikin route dengan awalan yang sama tanpa harus nulis ulang awalan tersebut di setiap route.

Layout

Ketika lo mau bikin layout yang berbeda buat beberapa route, lo bisa bungkus route tersebut di dalam fungsi layout. Misal, lo mau bikin layout buat halaman admin dan marketing, lo bisa bikin kayak gini:

app/routes.ts

Di dalam file masing-masing layout, lo bisa define <Outlet /> yang bakal nge-render child route. Contohnya, di file app/routes/admin/layout.tsx:

app/routes/admin/layout.tsx

Sama kayak layout yang ada di file app/root.tsx, layout ini juga nggak bakal di-render ulang ketika user pindah-pindah route yang ada di layout tersebut.

Styling

Secara bawaan, React Router udah ngeintegrasiin Tailwind CSS lewat Vite, jadi lo bisa langsung pake Tailwind CSS di project lo. Kalo lo mau pake opsi yang lain, tinggal ikutin aja panduan di dokumentasi library tersebut. Karena cara setup setiap library itu pasti beda-beda.

CSS

Secara mendasar, lo bisa pake CSS reguler dan bisa langsung import file-nya ke component React lo. Bisa liat contohnya di file app/root.tsx. Di sana ada import "app.css"; yang ngimpor file CSS global. Isi dari file tersebut kayak gini:

app.css

Kayak yang lo liat, di sini ada @import "tailwindcss"; yang ngimpor Tailwind CSS. Versi Tailwind CSS yang dipake di sini adalah versi 4. Itu kenapa lo nggak liat file tailwind.config.js di dalam project ini, karena buat versi 4 ini mereka pake file CSS buat ngekonfigurasinya.

Fonts

Kalo mau pake custom font, lo bisa taro file font-nya di dalam folder public dan import font tersebut di file CSS lo kayak gini contohnya:

style.css

Alternatifnya, lo bisa pake Google Fonts atau font CDN lainnya. Lo tinggal import aja file CSS-nya di file app/root.tsx kayak yang udah dicontohin sama React Router secara default:

app/root.tsx

Buat font CDN lainnya juga sama harusnya kalo mereka nyediain file CSS yang bisa di-import.

Kenapa pake <link> dan nggak pake @import di CSS aja?

Sebagai contoh, kalau lo pakai <link href="a.css"> buat load CSS, dan di dalam a.css ada @import 'b.css', maka browser bakal download a.css dulu, baru setelah itu download b.css. Ini bikin loading jadi sekuensial (satu-satu), bukan paralel. Alhasil, web lo jadi lebih lambat.

Padahal, kalau lo pakai banyak <link> langsung di HTML (atau @import di dalam <style> tag), browser bisa download semua CSS secara paralel—hasilnya lebih cepat.

Kesimpulannya, hindarin @import di file CSS secara umum. Kecuali kalau lo terpaksa (misalnya di CMS yang cuma bisa akses 1 file CSS), ya udah nggak masalah.

Meta Tags

Sebelum lebih jauh lagi, kita bahas dulu yang gampang, yaitu meta tags. Di dalem route module, lo bisa nge-export fungsi meta yang bakal nge-return meta tags yang lo pengen define:

app/routes/about-index.tsx

Tapi, karena React 19 punya built-in elemen <meta>, pake elemen tersebut lebih disarankan daripada pake fungsi meta:

app/routes/about-index.tsx

Cara tersebut menurut gue lebih bersih dan lebih gampang dibaca.

Lo udah belajar bikin route, sekarang lo perlu tau cara navigasi di antara route tersebut. Sama kayak di Next.js, React Router juga punya component <Link>. Bedanya, alih-alih pake href, lo pake to buat ngasih tahu path yang mau dituju.

Contohnya gini:

app/root.tsx

Selain <Link>, lo juga bisa pake component <NavLink>. Component tersebut mirip kayak <Link>, cuman dia punya fitur tambahan kayak buat nentuin apakah link tersebut aktif atau nggak. Ini berguna buat styling link yang aktif, contohnya:

<NavLink> with active class

Kalo mau navigasi programatis, lo bisa pake hook useNavigate. Hook ini mirip kayak useRouter di Next.js, tapi lebih simpel. Lo bisa pake navigate buat pindah ke route lain.

app/routes/home.tsx

Kalo navigasi yang butuh interaksi user, lo bisa pake <Link> atau <NavLink>. Tapi, kalo navigasi yang butuh logika atau kondisi tertentu, lo bisa pake useNavigate.

Data Loading

Iya, di Next.js namanya data fetching, tapi di React Router nyebutnya data loading. Data loading diperluin kalo lo butuh ambil data saat halaman di-render, misalnya buat nge-fetch data dari API atau database.

React Router punya dua fungsi buat data loading: loader dan clientLoader. Seperti yang gue bahas sebelumnya, loader dieksekusi di server saat halaman di-render, sedangkan clientLoader dieksekusi di client/browser.

Contohnya gini:

app/routes/product.tsx

loaderData bakal otomatis di-infer tipe datanya dari return value loader. Tapi, kalo lo pake fetch, loaderData bakal jadi any. Ini karena data yang lo fetch bisa punya tipe yang beda-beda, tergantung dari API yang lo panggil.

Contohnya:

app/routes/product.tsx

Di kasus kayak gini, lo bisa bikin type guard manual atau pake library kayak zod:

app/routes/product.tsx

Kalo misalnya lo pake ORM kayak Drizzle, loader juga bisa langsung nge-return model yang udah di-infer tipe datanya. Contohnya:

app/routes/product.tsx

Dengan cara ini, loaderData di dalam component Product bakal otomatis punya tipe yang sesuai dengan model products.

Fungsi loader ini juga bakal dihapus dari client bundle, jadi lo aman buat pake API yang cuman boleh jalan di server.

Actions

Data loading buat ngambil data, sedangkan action buat mutasi data. Sama kayak loader, action juga bisa dieksekusi di server lewat fungsi action atau di client lewat clientAction.

Bedanya dengan Server Actions di Next.js, actions di React Router nggak perlu ditaro di file khusus. Lo cuman perlu export fungsi action dari route module sama kayak lo export loader. Nantinya fungsi itu yang bakal dipanggil ketika ada form request.

Contohnya gini:

app/routes/admin/products.tsx

Penjelasan kodenya gini:

  • <Form> adalah component yang disediain React Router buat nge-handle form submission.
  • Ketika user submit form, hal ini yang terjadi:
    • action dieksekusi ketika ada form request dengan method POST, PUT, PATCH, atau DELETE.
    • request.formData() dipake buat ambil data dari form yang di-submit. Karena lo pake <Form>, maka tipe datanya adalah FormData.
    • Nama produk diambil dari form data dan disimpen ke variabel name.
    • Data produk di-update pake fungsi db.updateProduct({ name }) (ini contoh aja).
    • Terus data tersebut di-return sebagai actionData, yang bisa lo akses di dalam component Products.
    • Ketika action selesai dieksekusi tanpa error, semua loader data bakal di-revalidate supaya UI dan data tetep sinkron.
    • Terakhir, lo bisa akses nilai return dari action di dalam component lewat actionData.

Ketika lo pake <Form> buat submit form, dia bakal nambahin entry ke history browser, jadi user bisa kembali ke halaman sebelumnya pake tombol back di browser. Kalo lo nggak mau behavior ini, lo bisa pake yang namanya fetcher:

app/routes/admin/products.tsx

Ketika pake fetcher, buat ngambil data return dari action, lo bisa pake fetcher.data.

Di kasus nyata, lo biasanya butuh ngevalidasi data sebelum disimpen. Nggak ada cara spesial buat ini, standar aja kayak gini:

app/routes/admin/products.tsx

Ketika lo mau nge-return error dari action, lo bisa pake fungsi data dan ngasih status code yang sesuai. Hal ini diperluin supaya mencegah React Router nge-revalidate semua loader data yang ada di halaman tersebut.

Authentication

Enaknya di React Router, dia udah nyediain session storage yang bisa lo pake buat bikin sistem autentikasi. Jadi, lo nggak perlu ribet-ribet mikirin solusi sendiri. Ini cukup kalo lo pengen kontrol penuh atas sistem autentikasi lo.

Lo bisa pake createSessionStorage dari React Router buat bikin session storage. Contohnya:

app/libs/session.ts

Session di atas lo bisa pake buat nyimpen data user yang lagi login, misalnya userId (dalam contoh ini) atau data flash (pesan sekali tampil) buat ngasih feedback ke user setelah mereka melakukan aksi tertentu.

Kalo lo liat lagi, di atas ada tiga fungsi yang di-export:

  • getSession: buat ambil session dari cookie.
  • commitSession: buat simpen session ke cookie.
  • destroySession: buat hapus session dari cookie.

Sistem session di React Router ini nggak di-spread lewat middleware, jadi lo harus panggil fungsi getSession setiap kali lo butuh akses session di dalam route module lo.

Sebagai contoh, bikin halaman login:

app/routes/login.tsx

Penjelasannya gini:

  • Di loader, kita ngecek apakah user udah login dengan ngecek apakah session punya userId:
    • Kalo udah, kita redirect ke halaman utama.
    • Kalo belum, kita ambil data flash error dari session dan return ke component buat ditampilin.
  • Ketika user submit form, action bakal dipanggil dan ini yang terjadi:
    • Di sini, kita ambil session dari cookie dan ngecek apakah username dan password yang dimasukin valid.
    • Kalo valid, simpen userId ke session dan redirect ke halaman utama.
    • Kalo nggak valid, set flash message error ke session dan redirect kembali ke halaman login.

Hal yang perlu diinget, ketika lo nge-set sesuatu di session, lo harus nge-commit session tersebut dengan commitSession(session) supaya perubahan yang lo buat ke session bisa disimpen ke cookie.

Dan kalo lo mau ngehapus session, lo bisa pake destroySession(session):

app/routes/logout.tsx

Cara ini cukup buat bikin sistem autentikasi yang simpel. Kalo lo butuh solusi yang lebih kompleks, lo bisa pake library kayak better-auth atau OpenAUTH.

Resource Routes

Resource routes ini simpelnya adalah route yang nggak punya UI, jadi lo cuman export fungsi loader atau action aja. Persis, fungsinya kayak API Routes di Next.js. Maka dari itu, kita coba bikin "API Routes" di React Router pake resource routes.

Pertama, lo bikin route-nya dulu di app/routes.ts:

app/routes.ts

Kemudian lo bikin file app/routes/api/products.ts:

app/routes/api/products.ts

Selesai! Sekarang lo punya resource route yang bisa diakses di /api/products.

Fungsi loader bisa lo pake buat nge-handle request GET ke /api/products. Sementara buat request POST, PUT, DELETE, atau PATCH, bakal di-handle sama fungsi action:

app/routes/api/products.ts

Kalo dalam suatu kasus lo pake fetch dan ngirim payload JSON, lo bisa pake request.json():

app/routes/api/products.ts

Sesuai sama namanya, resource routes ini nggak khusus buat API aja. Lo bisa ngelakuin sesuatu yang lain juga, misalnya nge-handle webhook, render PDF, atau nge-stream response dari LLM.

Streaming

Ketika lo bikin halaman yang nge-load beberapa data sekaligus, lo bisa pake streaming supaya halaman tersebut bisa ditampilkan lebih cepat alias nggak nge-block proses rendering. Konsepnya sama kayak yang ada di Next.js, cuman beda caranya aja.

Anggap aja lo punya halaman detail produk yang nampilin inforasi produk dan review produk. Dari kedua data tersebut, yang paling penting adalah informasi produk, sedangkan review produk bisa ditampilin belakangan.

Contohnya kayak gini:

app/routes/product.tsx

Penjelasannya gini:

  • Di loader, kita ambil data produk dan review produk.
    • Data informasi produk ini penting, jadi kita await supaya bisa langsung ditampilin.
    • Sedangkan review produk kita nggak await, supaya bisa di-stream.
  • Di dalam component Product, kita pake <Await> dan <Suspense> buat nge-handle data review yang di-stream.
    • <Await> bakal nunggu data review resolve dan nge-render list review lewat pattern render props.
    • <Suspense> bakal nampilin fallback UI (dalam hal ini, "Loading reviews...") selama data review belum tersedia.

Basic-nya kayak gitu. Kalo seaindainya lo pake React 19, lo bisa pke React.use. Tapi, perlu diabstrak jadi dua komponen:

app/routes/product.tsx

Sementara isi dari komponen ProductReviews adalah:

app/components/ProductReviews.tsx

Bebas aja lo mau pake yang mana.

Error Handling

Setiap ada error yang terjadi di aplikasi lo, React Router bakal nge-trigger ErrorBoundary yang ada di app/root.tsx. Lo bisa coba test ini dengan bikin error kayak gini:

app/routes/product.tsx

Dengan begini, ErrorBoundary yang ada di app/root.tsx bakal di-render.

Kalo seandainya lo rajin dan mau bikin error handling yang lebih spesifik buat setiap route, lo bisa bikin ErrorBoundary di dalam route module itu sendiri.

app/routes/product.tsx

Variable error di dalam ErrorBoundary ini bisa beda-beda tergantung dari error yang terjadi. Maka dari itu lo bisa cek tipe error-nya kayak yang udah dicontohin di file app/root.tsx.

app/root.tsx

Kalo error yang terjadi hasil dari data dengan status code dan text, branch isRouteErrorResponse bakal dipake. Contohnya:

app/routes/product.tsx

Selain itu, kalo error yang terjadi adalah instance dari Error, branch error instanceof Error bakal dipake. Contohnya:

app/routes/product.tsx

Kalo lo bikin beberapa ErrorBoundary, React Router bakal nge-render ErrorBoundary yang paling dekat dengan sumber error terjadi. Misal, kalo ada error di dalam Product component, maka ErrorBoundary yang ada di Product yang bakal di-render. Kalo nggak ada, maka ErrorBoundary parent-nya yang bakal di-render, sampai ke ErrorBoundary yang ada di app/root.tsx.

Rendering Strategies

Secara default, React Router bakal nge-render halaman secara server-side pake mekanisme SSR (Server-Side Rendering). Ini sama kayak Next.js versi pages router. Di samping itu, React Router juga support dua strategi rendering lainnya, yaitu:

  • Client-Side Rendering: Halaman di-render di client/browser, jadi nggak ada SSR.
  • Static Pre-rendering: Halaman di-render di server saat build time, jadi hasilnya adalah HTML statis.

Buat ngubah strategi rendering, lo bisa atur konfigurasinya di file react-router.config.ts. Ini contohnya kalo lo mau pake Client-Side Rendering:

react-router.config.ts

Dengan nge-disable SSR, artinya alih-alih pake loader dan action, lo bakal pake clientLoader dan clientAction buat nge-fetch dan mutasi data di client/browser. Ini basically lo bikin Single Page Application (SPA) pake React Router.

Penutup

Oke, segitu aja dulu. Buat pelajaran yang lebih spesifik, gue bakal bahas di tulisan yang lain. Terima kasih udah mau baca!