TanStack Router: Routing yang Sepenuhnya Type-Safe di React

Foto oleh Norfolks disused railways via Wikimedia Commons (CC BY 4.0)
Ya. Path params, search params, dan data loader semuanya disimpulkan melalui TypeScript ketika Anda memakai hook yang di-scope ke route seperti Route.useParams, Route.useSearch, dan Route.useLoaderData. Untuk memperluas pengetikan itu ke helper global seperti Link dan useNavigate, Anda menambahkan blok declaration merging Register sekali saja. Setelah itu compiler menangkap route dan params yang tidak valid sebelum runtime.
Anda mendeklarasikan fungsi validateSearch pada route, biasanya didukung schema Zod, yang mem-parse dan memvalidasi search string mentah menjadi objek bertipe. Setiap hook dan panggilan navigasi kemudian melihat bentuk yang tervalidasi dan bertipe. Memakai .catch() milik Zod membuat param rusak jatuh ke default secara diam-diam alih-alih melempar error ke pengguna.
File-based routing memakai plugin untuk menghasilkan route tree dan semua keterkaitan tipe dari file route di disk, dan menangani code splitting otomatis. Code-based routing berarti Anda membangun tree dengan tangan, menyambungkan setiap anak ke induknya dengan getParentRoute. Keduanya type-safe, tetapi file-based adalah default yang direkomendasikan dan butuh jauh lebih sedikit boilerplate.
Loader tidak berjalan ulang pada setiap perubahan search param secara bawaan. Anda mendeklarasikan nilai search mana yang dipedulikan loader menggunakan loaderDeps, dan hanya itu yang memicu reload. Ini menjaga caching tetap benar dan mencegah perubahan filter yang tak terkait mengambil data ulang tanpa perlu.
Sering kali ya. Loader adalah batas pengambilan data yang menjamin data ada saat route ter-render, tetapi bukan lapisan cache penuh. Untuk deduplikasi request, refetch latar belakang, dan mutasi, pasangkan loader dengan TanStack Query — biasanya dengan memanggil queryClient.ensureQueryData di dalam loader.

Foto oleh Norfolks disused railways via Wikimedia Commons (CC BY 4.0)
Ringkasan Utama
TanStack Router membuat routing type-safe dari ujung ke ujung: path params, URL search params, dan data loader semuanya mengalir melalui inferensi TypeScript. Anda memvalidasi search params sekali dengan sebuah schema, loader memegang pengambilan data async, dan file-based routing membuat route tree otomatis sehingga compiler menangkap link rusak sebelum runtime.
Kebanyakan router React memperlakukan URL sebagai kantong string tanpa tipe. Anda menavigasi ke sebuah path, membaca param, dan berharap param itu ada dan berbentuk seperti yang Anda asumsikan. Pada sebuah layanan yang saya kelola, saya kehilangan satu sore gara-gara rename yang membuat tiga komponen menunjuk ke route yang sudah tidak ada — build lolos, halaman 404 di production. TanStack Router adalah router pertama yang saya pakai yang mengubah kelas bug itu menjadi compile error.
Router ini mencapai v1 stabil di akhir 2023 dan stabil di production sejak saat itu. Di 2026 ini adalah router yang dipilih orang ketika mereka ingin TypeScript benar-benar memahami navigasi mereka, dan ini adalah fondasi di bawah TanStack Start untuk server-side rendering. Artikel ini membahas tiga hal yang membuatnya berbeda: typed params, search state tervalidasi, dan loader — lalu membandingkan file-based dan code-based routing agar Anda bisa memilih yang tepat.
Ketika Anda mendefinisikan route dengan segmen dinamis, router menyimpulkan nama dan tipe param dari definisi route. Anda tidak menganotasi apa pun secara manual, dan Anda tidak bisa membaca param yang tidak dideklarasikan route. Hook-nya di-scope ke route, jadi tipe kembaliannya persis params yang dimiliki route itu — tidak ada optional di mana-mana, tidak ada tebakan string-atau-undefined.
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
component: PostComponent,
})
function PostComponent() {
// postId is typed as string — inferred from the route path
const { postId } = Route.useParams()
return <div>Post {postId}</div>
}Untuk komponen bersama yang di-render di bawah beberapa route, berikan opsi from ke sebuah hook untuk memilih konteks route mana yang Anda mau, atau gunakan strict: false untuk menerima union dari kemungkinan params. Itu membuat komponen helper tetap dapat dipakai ulang tanpa kehilangan semua informasi tipe.
Ini adalah fitur yang mengubah cara saya memikirkan URL. Search params — bagian setelah tanda tanya — biasanya state paling berantakan di aplikasi React: paginasi, filter, urutan sort, semuanya diserialisasi sebagai string dan di-parse ulang di mana-mana. TanStack Router memperlakukan search string sebagai objek bertipe kelas satu. Anda mendeklarasikan schema validateSearch pada route, dan sejak itu objek yang sudah di-parse dan tervalidasi adalah apa yang dilihat setiap hook dan Link.
// src/routes/shop.products.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const productSearchSchema = z.object({
page: z.number().catch(1),
filter: z.string().catch(''),
sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
})
export const Route = createFileRoute('/shop/products')({
validateSearch: (search) => productSearchSchema.parse(search),
component: Products,
})
function Products() {
// sort is 'newest' | 'oldest' | 'price', not a loose string
const { page, filter, sort } = Route.useSearch()
// ...
}Penggunaan .catch() alih-alih .default() itu disengaja. .catch() berarti param yang rusak atau hilang secara diam-diam jatuh ke nilai fallback alih-alih melempar error validasi ke pengguna. Sebuah link berbagi dengan nilai sort sampah tetap ter-render; ia hanya memakai default. Itu hampir selalu yang Anda inginkan untuk URL state yang bisa diedit orang asing dengan tangan.
Sebuah route loader mengangkat pekerjaan async keluar dari komponen. Alih-alih useEffect yang berjalan setelah render, loader berjalan sebagai bagian dari penyelesaian route — paralel dengan loader saudara, dengan caching, dan dengan preloading saat pengguna menunjukkan niat dengan hover pada link. Loader memegang batas data, dan nilai kembaliannya disimpulkan ke dalam useLoaderData tanpa pengetikan manual.
Bagian halusnya adalah bagaimana loader dan search params terhubung. Loader tidak otomatis berjalan ulang ketika search param apa pun berubah — itu akan refetch pada setiap toggle filter yang tak terkait. Anda mendeklarasikan nilai search mana yang menjadi dependensi loader dengan loaderDeps, dan hanya itu yang memicu reload. Ini menjaga caching tetap benar dan menghindari waterfall klasik dari over-fetching.
// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
validateSearch: z.object({
offset: z.number().int().nonnegative().catch(0),
}),
// only `offset` re-triggers the loader
loaderDeps: ({ search: { offset } }) => ({ offset }),
loader: async ({ deps: { offset } }) => fetchPosts({ offset }),
component: Posts,
})
function Posts() {
const posts = Route.useLoaderData() // typed as the return of fetchPosts
// ...
}Loader adalah batas pengambilan data, bukan lapisan cache penuh. Untuk dedup request, refetch latar belakang, dan mutasi Anda tetap membutuhkan TanStack Query. Pola umumnya adalah loader yang memanggil queryClient.ensureQueryData sehingga route menjamin data ada sementara Query memegang siklus hidup cache.
TanStack Router mendukung baik mendefinisikan route sebagai file di disk maupun membangun route tree dengan tangan dalam kode. Keduanya sepenuhnya type-safe, tetapi mereka sampai ke sana dengan cara berbeda. File-based routing memakai plugin yang membaca file route Anda dan menghasilkan route tree beserta semua keterkaitan tipe untuk Anda. Code-based routing berarti Anda membangun tree sendiri, menyambungkan setiap anak ke induknya dengan getParentRoute agar anak mewarisi tipe induk.
| Aspek | File-based routing | Code-based routing |
|---|---|---|
| Route tree | Dibuat otomatis oleh plugin | Dirakit dengan tangan lewat array children |
| Keterkaitan tipe | Disambungkan untuk Anda; lebih sedikit boilerplate | Anda mengelola getParentRoute manual |
| Code splitting | Otomatis per file route | Manual, Anda mengatur lazy import |
| Keterlacakan | Struktur URL mencerminkan file tree | Berada di mana pun Anda taruh di kode |
| Paling cocok untuk | Kebanyakan aplikasi; default yang direkomendasikan | Pembuatan route dinamis/programatik |
Panduan resminya jelas: file-based routing adalah default yang direkomendasikan, dan itulah yang dipakai TanStack Start. Ia menaikkan plafon type-safety karena generator menghasilkan keterkaitan yang akan melelahkan dan rawan error jika dipelihara dengan tangan. Saya memakai code-based routing hanya ketika route harus dibuat dinamis saat runtime — misalnya sistem plugin — di mana file di disk tidak bisa menggambarkan tree. Untuk selebihnya, biarkan plugin yang menyambungkan.
Typed params dan loader bekerja di dalam route tanpa upacara ekstra, tetapi untuk membuat export tingkat atas seperti Link dan useNavigate sepenuhnya bertipe terhadap route tree Anda, Anda mendaftarkan router dengan declaration merging. Ini adalah satu baris yang mengalirkan tipe route persis aplikasi Anda ke helper navigasi global, sehingga prop to yang tidak valid pada Link menjadi garis merah di editor Anda.
// src/main.tsx
const router = createRouter({ routeTree })
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}Setelah registrasi, refactor path route jadi aman: ganti nama file, jalankan generator, dan setiap Link dan panggilan useNavigate yang rusak menyala di TypeScript. Itulah seluruh janjinya — router memahami URL Anda cukup baik untuk menggagalkan build alih-alih mengirim 404.
Jika Anda selama ini berjuang dengan navigasi bertipe string dan pengambilan data useEffect, TanStack Router layak dengan biaya migrasinya. Mulai dengan file-based routing, validasi search params setiap route dengan schema, pindahkan pengambilan data ke dalam loader yang disambungkan lewat loaderDeps, dan tambahkan blok Register. Imbalannya adalah basis kode di mana URL adalah state nyata dan bertipe — dan di mana compiler, bukan production, yang memberi tahu Anda saat sebuah link rusak.