Sebagian besar project saya dibangun dengan React dan Next.js, termasuk situs ini. Belakangan saya makin sering memakai SvelteKit, dan minggu pertama penuh dengan pertanyaan yang sama: page.tsx saya taruh di mana? Apa pengganti useEffect? Saya pakai shadcn/ui dan React Hook Form di mana-mana. Sekarang pakai apa?
Tulisan ini untuk kamu yang sudah paham Next.js dan belum pernah menulis satu baris pun kode Svelte. Setiap ide baru ditunjukkan berdampingan dengan kode Next.js yang sudah biasa kamu tulis.
Kabar baiknya dulu: SvelteKit dan Next.js menyelesaikan masalah yang sama. Routing berbasis file, server rendering, layout, API route, form. Sebagian besar pengetahuanmu tetap terpakai. Perubahan terbesarnya bukan di framework, tapi perpindahan dari React ke Svelte. 🧡
Versi di tulisan ini: Next.js 16 (App Router) dan SvelteKit 2 dengan Svelte 5 (runes). Kalau kamu menemukan tutorial lama yang memakai
export letatau$:, itu Svelte 4. Lewati saja.
Svelte dan SvelteKit dalam satu menit#
Kalau kamu benar-benar baru, ini petanya:
- Svelte adalah lapisan UI, seperti React. Kamu menulis komponen di file
.svelte. - SvelteKit adalah framework aplikasinya, seperti Next.js. Routing, server rendering, data loading, form, dan deployment.
- Svelte adalah compiler. Saat build, komponenmu diubah jadi JavaScript biasa yang ringkas. Tidak ada virtual DOM dan tidak ada runtime besar di browser.
- SvelteKit berjalan di atas Vite, jadi dev server-nya cepat dan ekosistem plugin Vite bisa dipakai.
Memulai project kira-kira seperti ini:
npx sv create my-app
cd my-app
npx sv add tailwindcss eslint vitest
npm run devsv adalah CLI Svelte. sv add menyiapkan tool yang umum untukmu: Tailwind, ESLint, Prettier, Vitest, Playwright, Drizzle, Paraglide (i18n), mdsvex (markdown), Storybook, dan lainnya.
Gambaran besarnya#
| Topik | Next.js | SvelteKit |
|---|---|---|
| Library UI | React | Svelte |
| Cara UI diperbarui | Virtual DOM, menjalankan ulang fungsi komponen | Compiler, hanya memperbarui node DOM yang berubah |
| File komponen | .tsx (JSX) |
.svelte (HTML + <script> + <style>) |
| Bundler / dev server | Turbopack | Vite |
| Folder route | app/ |
src/routes/ |
| Data di server | Server Components (komponen async) |
Fungsi load |
| Mutasi | Server Actions | Form actions (dan remote functions yang lebih baru) |
| Middleware | proxy.ts (dulu middleware.ts) |
hooks.server.ts |
| Caching | Bawaan ("use cache", revalidate) |
Tidak ada data cache, pakai HTTP header |
| Style | CSS Modules, Tailwind, dll. | <style> scoped secara default, Tailwind juga bisa |
| UI kit populer | shadcn/ui | shadcn-svelte |
| Form | React Hook Form, Formik | Superforms (+ Formsnap) |
| Target deploy | Paling enak di Vercel, Node, atau static export | Adapter: Node, Vercel, Netlify, Cloudflare, static, ... |
Jangan khawatir kalau ada baris yang terlihat aneh. Kita bahas satu per satu.
1. Cara berpikirnya: tidak ada re-render#
Di React, komponenmu adalah fungsi yang dijalankan ulang setiap kali state berubah. Itu sebabnya kamu butuh useMemo, useCallback, dependency array, dan React.memo.
Di Svelte, <script> di komponenmu hanya berjalan sekali. Compiler mencari tahu bagian HTML mana yang bergantung pada variabel mana, lalu hanya memperbarui bagian itu ketika variabelnya berubah.
Satu ide ini menjelaskan sebagian besar perbedaan di bawah:
- Tidak ada dependency array. Svelte melacak dependensi untukmu.
- Tidak ada
useCallback. Fungsi hanya dibuat sekali, jadi sudah stabil. - Tidak ada stale closure. Variabel yang kamu baca selalu berisi nilai terbaru.
- Bundle lebih kecil. Svelte hanya mengirim runtime mungil, bukan library sebesar React.
2. Komponen: JSX vs file .svelte#
React
// components/Counter.tsx
"use client";
import { useState } from "react";
export function Counter({ start = 0 }: { start?: number }) {
const [count, setCount] = useState(start);
const double = count * 2;
return (
<button className="btn" onClick={() => setCount(count + 1)}>
{count} (double: {double})
</button>
);
}Svelte
<!-- src/lib/components/Counter.svelte -->
<script lang="ts">
let { start = 0 }: { start?: number } = $props();
let count = $state(start); // hanya nilai awal, seperti useState(start)
let double = $derived(count * 2);
</script>
<button class="btn" onclick={() => count++}>
{count} (double: {double})
</button>
<style>
.btn {
padding: 0.5rem 1rem;
}
</style>Yang sama
- Props, state, dan nilai turunan semuanya ada. Hanya namanya yang berbeda.
- Event handler adalah fungsi biasa. Svelte 5 memakai
onclick, yaitu nama atribut HTML biasa. - TypeScript langsung bisa dipakai dengan
lang="ts".
Yang berbeda
- Tidak ada
"use client". Setiap komponen Svelte dirender di server lalu di-hydrate di browser. Tidak ada Server Components. - Kamu mengubah state dengan meng-assign nilainya.
count++, bukansetCount(count + 1). Array dan object juga bisa:todos.push(newTodo)langsung jalan. - Pakai
class, bukanclassName. Danfor, bukanhtmlFor. Ini memang HTML. <style>otomatis scoped ke komponen. Tidak perlu CSS Modules.- Satu komponen per file. Nama file adalah nama komponennya.
Fungsi-fungsi $ itu ($state, $derived, $props, $effect) disebut runes. Dengan runes, Svelte 5 tahu mana yang reaktif. Ini pemetaan hooks kamu:
| React | Svelte 5 |
|---|---|
useState(0) |
let count = $state(0) |
useMemo(() => a * b, [a, b]) |
let total = $derived(a * b) |
useEffect(() => { ... }, [x]) |
$effect(() => { ... }) (tanpa dependency array) |
function C(props) |
let { ... } = $props() |
children / render props |
Snippet + {@render children()} |
useRef untuk node DOM |
bind:this={node} |
useRef untuk nilai |
Variabel let biasa |
useContext / createContext |
getContext / setContext |
value + onChange |
bind:value={name} |
<Suspense> |
{#await promise} |
useCallback, React.memo |
Tidak perlu |
Sintaks template#
JSX memakai JavaScript untuk semuanya. Svelte punya beberapa blok kecil sebagai gantinya:
{#if user}
<p>Hi, {user.name}</p>
{:else}
<a href="/login">Log in</a>
{/if}
<ul>
{#each todos as todo (todo.id)}
<li class={{ done: todo.done }}>{todo.title}</li>
{:else}
<li>Nothing to do 🎉</li>
{/each}
</ul>Bagian (todo.id) adalah key-nya. di dalam {#each} tampil ketika list-nya kosong. Dan class menerima object dan array, seperti clsx.
children menjadi snippet#
Di React, kamu mengoper children lalu merendernya. Di Svelte 5, children adalah snippet, dan kamu merendernya dengan {@render}:
<!-- Card.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
let { title, children }: { title: string; children: Snippet } = $props();
</script>
<section class="card">
<h2>{title}</h2>
{@render children()}
</section><Card title="Hello">
<p>This is the content.</p>
</Card>Butuh "slot" bernama seperti header atau footer? Deklarasikan dengan {#snippet footer()}...{/snippet} di dalam tag komponen, dan snippet itu masuk sebagai props. Idenya sama dengan mengoper JSX lewat props di React.
3. Routing: app/ vs src/routes/#
Kedua framework memakai folder untuk URL. Yang berubah adalah nama filenya. Di SvelteKit, setiap file khusus diawali dengan +.
Next.js (app/) |
SvelteKit (src/routes/) |
Fungsinya |
|---|---|---|
page.tsx |
+page.svelte |
UI halaman |
layout.tsx |
+layout.svelte |
Layout bersama |
route.ts |
+server.ts |
Endpoint API (GET, POST, ...) |
error.tsx |
+error.svelte |
UI error |
not-found.tsx |
+error.svelte (cek page.status === 404) |
Halaman 404 |
loading.tsx |
Tidak ada file. Pakai streaming + {#await} |
UI loading |
Data di komponen halaman async |
+page.ts atau +page.server.ts |
Memuat data halaman |
[slug], [...slug], (group) |
Sama | Dinamis, catch-all, grup |
[[...slug]] |
[...slug] (sudah cocok dengan nol segmen) |
Catch-all opsional |
@modal, (.)photo |
Tidak ada padanan langsung, pakai shallow routing | Parallel / intercepting route |
middleware.ts / proxy.ts |
src/hooks.server.ts |
Jalan di setiap request |
public/ |
static/ |
File statis |
Layout merender halamannya dengan {@render children()}, seperti {children} di layout Next.js:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import Nav from "$lib/components/Nav.svelte";
import "../app.css";
let { children } = $props();
</script>
<Nav />
<main>{@render children()}</main>$lib adalah alias bawaan untuk src/lib. Di sanalah komponen dan utility kamu tinggal, seperti @/ di Next.js.
4. Data loading: Server Components vs load#
Ini perubahan terbesar dalam cara kamu berpikir.
Di Next.js, komponennya sendiri yang mengambil data:
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await db.post.findUnique({ where: { slug } });
if (!post) notFound();
return <article>{post.title}</article>;
}Di SvelteKit, data datang dari fungsi load di file terpisah, dan halaman menerimanya sebagai prop data:
// src/routes/blog/[slug]/+page.server.ts
import { error } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ params }) => {
const post = await db.post.findUnique({ where: { slug: params.slug } });
if (!post) error(404, "Post not found");
return { post };
};<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageProps } from "./$types";
let { data }: PageProps = $props();
</script>
<svelte:head>
<title>{data.post.title}</title>
</svelte:head>
<article>
<h1>{data.post.title}</h1>
</article>Yang berbeda
- Data dan UI ada di file terpisah. Awalnya terasa seperti kebanyakan file. Lama-lama terasa rapi: satu file untuk "data apa", satu file untuk "tampilannya seperti apa".
- Type dibuatkan otomatis.
./$typestahu params route kamu dan apa yang dikembalikanload. - Ada dua jenis
load:+page.server.tsberjalan hanya di server. Pakai untuk database, secret, dan cookie.+page.tsberjalan di server untuk halaman pertama, lalu di browser untuk navigasi berikutnya. Pakai untuk memanggil API publik.
- Layout juga bisa punya fungsi
load(+layout.server.ts). Semua halaman di bawahnya mendapat data itu. - Metadata cukup ditulis di
<svelte:head>di halaman, memakaidatayang sama. Tidak perlugenerateMetadataterpisah.
Streaming menggantikan loading.tsx dan <Suspense>. Kembalikan promise tanpa await, lalu render dengan {#await}:
// +page.server.ts
export const load: PageServerLoad = async ({ params }) => {
return {
post: await getPost(params.slug), // tunggu yang ini
comments: getComments(params.slug), // stream yang ini
};
};{#await data.comments}
<p>Loading comments...</p>
{:then comments}
{#each comments as comment (comment.id)}
<p>{comment.body}</p>
{/each}
{/await}5. Mutasi: Server Actions vs form actions#
Di Next.js, kamu menulis fungsi "use server" lalu mengopernya ke <form action>. Versi SvelteKit adalah form actions: fungsi bernama di +page.server.ts yang menerima POST form biasa.
// src/routes/todos/+page.server.ts
import { fail } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
export const load: PageServerLoad = async () => {
return { todos: await db.todo.findMany() };
};
export const actions: Actions = {
create: async ({ request }) => {
const form = await request.formData();
const title = form.get("title");
if (typeof title !== "string" || !title.trim()) {
return fail(400, { error: "Title is required" });
}
await db.todo.create({ data: { title } });
},
};<!-- src/routes/todos/+page.svelte -->
<script lang="ts">
import { enhance } from "$app/forms";
import type { PageProps } from "./$types";
let { data, form }: PageProps = $props();
</script>
<form method="POST" action="?/create" use:enhance>
<input name="title" />
<button>Add</button>
{#if form?.error}<p class="error">{form.error}</p>{/if}
</form>
<ul>
{#each data.todos as todo (todo.id)}
<li>{todo.title}</li>
{/each}
</ul>- Tanpa JavaScript, ini form HTML biasa. Tetap berfungsi. ✅
use:enhancemeng-upgrade-nya: tanpa reload halaman penuh, danloadotomatis dijalankan ulang, jadi list-nya ikut ter-update. Kamu tidak perlurevalidatePath.- Apa pun yang kamu kembalikan lewat
fail()muncul di propform.
SvelteKit juga sedang mengembangkan remote functions (query, form, command di file .remote.ts) yang bisa dipanggil dari komponen mana pun, mirip Server Actions. Saat tulisan ini dibuat, fitur ini masih eksperimental dan API-nya sempat berubah di antara versi minor. Pelajari load dan form actions dulu.
6. Stack kamu: pengganti shadcn/ui, React Hook Form, Formik...#
Ini pertanyaan yang paling sering muncul di kepala saya, jadi bagian ini paling panjang. Buka package.json kamu dan cocokkan dengan tabel-tabel di bawah.
Library komponen UI#
| Yang kamu pakai di Next.js | Pakai di SvelteKit | Catatan |
|---|---|---|
| shadcn/ui | shadcn-svelte | Model copy-paste yang sama, variabel tema Tailwind yang sama, nama komponen yang sama. Perpindahan paling gampang. |
| Radix UI / Headless UI | Bits UI, Melt UI | Headless dan aksesibel. API Bits UI terasa seperti Radix, dan shadcn-svelte dibangun di atas Bits UI. |
| Chakra UI | Ark UI (@ark-ui/svelte) |
Dibuat oleh tim Chakra. Headless, hanya Svelte 5. |
| MUI | SMUI | Kit Material Design dari komunitas. Tidak ada MUI resmi. |
| Mantine, Ant Design | Skeleton, Flowbite Svelte, Carbon | Tidak ada port 1:1. Pilih kit yang cocok dengan gayamu. |
| daisyUI | daisyUI | Cuma CSS. Langsung jalan. |
Kalau kamu pakai shadcn/ui sekarang, setup shadcn-svelte akan terasa familiar:
npx shadcn-svelte@latest init
npx shadcn-svelte@latest add button dialog form<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
</script>
<Dialog.Root>
<Dialog.Trigger>
{#snippet child({ props })}
<Button {...props} variant="outline">Edit profile</Button>
{/snippet}
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Edit profile</Dialog.Title>
</Dialog.Content>
</Dialog.Root>Bagian {#snippet child({ props })} adalah cara Bits UI melakukan asChild ala Radix. Kamu menyebar props trigger ke elemen milikmu sendiri.
Form: React Hook Form dan Formik → Superforms#
Tidak ada hook useForm() di Svelte, dan biasanya kamu memang tidak butuh. bind:value sudah menyinkronkan input dan state. Untuk form sungguhan (validasi, error, state loading), standar di komunitas adalah Superforms. Superforms bekerja bersama form actions: satu schema Zod memvalidasi di server dan di client.
| React Hook Form / Formik | Superforms |
|---|---|
useForm({ resolver: zodResolver(schema) }) |
superForm(data.form, { validators: zod4Client(schema) }) |
register("email") |
bind:value={$form.email} |
formState.errors / errors |
$errors |
formState.isSubmitting / isSubmitting |
$submitting |
formState.dirtyFields / dirty |
$tainted |
handleSubmit(onSubmit) |
use:enhance + form action |
| Validasi server: kamu tulis lagi | Schema yang sama, di dalam action |
Ini form sign-up. Pertama, versi React Hook Form yang biasa kamu tulis:
// app/signup/page.tsx
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { schema, type SignUp } from "./schema";
import { signUp } from "./actions";
export default function SignUpPage() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<SignUp>({ resolver: zodResolver(schema) });
return (
<form onSubmit={handleSubmit(signUp)}>
<input type="email" {...register("email")} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register("password")} />
{errors.password && <span>{errors.password.message}</span>}
<button disabled={isSubmitting}>Sign up</button>
</form>
);
}Form yang sama dengan Superforms. File schema-nya tidak berubah:
// src/routes/signup/schema.ts
import { z } from "zod";
export const schema = z.object({
email: z.email(),
password: z.string().min(8),
});// src/routes/signup/+page.server.ts
import { fail } from "@sveltejs/kit";
import { message, superValidate } from "sveltekit-superforms";
import { zod4 } from "sveltekit-superforms/adapters";
import type { Actions, PageServerLoad } from "./$types";
import { schema } from "./schema";
export const load: PageServerLoad = async () => {
return { form: await superValidate(zod4(schema)) };
};
export const actions: Actions = {
default: async ({ request }) => {
const form = await superValidate(request, zod4(schema));
if (!form.valid) return fail(400, { form });
await createUser(form.data);
return message(form, "Welcome aboard!");
},
};<!-- src/routes/signup/+page.svelte -->
<script lang="ts">
import { superForm } from "sveltekit-superforms";
import { zod4Client } from "sveltekit-superforms/adapters";
import { schema } from "./schema";
let { data } = $props();
const { form, errors, message, submitting, enhance } = superForm(data.form, {
validators: zod4Client(schema),
});
</script>
<form method="POST" use:enhance>
<input name="email" type="email" bind:value={$form.email} />
{#if $errors.email}<span>{$errors.email}</span>{/if}
<input name="password" type="password" bind:value={$form.password} />
{#if $errors.password}<span>{$errors.password}</span>{/if}
<button disabled={$submitting}>Sign up</button>
{#if $message}<p>{$message}</p>{/if}
</form>Beberapa hal yang perlu diperhatikan:
- Tanda
$di$formdan$errorsbukan rune. Itu sintaks Svelte untuk membaca store (nilai reaktif versi lama yang masih dipakai Superforms). Di dalam komponen,$storeartinya "nilainya saat ini". - Validasi jalan di client sebelum submit dan di server di dalam action. Kamu tidak menulisnya dua kali.
- Kalau JavaScript mati, form tetap ter-submit dan menampilkan error dari server.
- Kalau sekarang kamu pakai
<Form>dari shadcn/ui, Form di shadcn-svelte dibangun di atas Superforms + Formsnap, yang menambahkan label, deskripsi, dan error yang aksesibel.
Datang dari Formik? Pemetaannya rapi: values → $form, errors → $errors, dirty → $tainted, isSubmitting → $submitting. Mau form yang hanya di client tanpa server action? TanStack Form punya adapter Svelte resmi dengan API yang mungkin sudah kamu kenal dari React.
Schema Zod, Valibot, atau Yup kamu pindah tanpa perubahan. Mereka memang tidak pernah tahu soal React.
Sisanya, singkat saja#
| Yang kamu pakai di Next.js | Pakai di SvelteKit |
|---|---|
| TanStack Query | @tanstack/svelte-query, tapi coba load + invalidate() dulu |
| SWR | load + invalidate() (bawaan) |
| tRPC | trpc-sveltekit, atau remote functions |
| Zustand, Jotai, Redux | $state di file .svelte.ts, dibagikan lewat context |
| Framer Motion | Bawaan: svelte/transition, svelte/animate, svelte/motion |
| NextAuth / Auth.js | @auth/sveltekit |
| Better Auth, Supabase | Package yang sama, dipasang di hooks.server.ts |
| Clerk | svelte-clerk (komunitas) |
| next-intl, react-i18next | Paraglide JS (npx sv add paraglide) |
| lucide-react | @lucide/svelte |
| sonner | svelte-sonner |
| next-themes | mode-watcher |
| TanStack Table | Data Table shadcn-svelte, atau @tanstack/svelte-table (v9 beta) |
| Recharts | LayerChart (yang dipakai Charts di shadcn-svelte) |
next/image |
@sveltejs/enhanced-img |
next/font |
Fontsource (tidak ada optimizer bawaan) |
| MDX | mdsvex |
| React Testing Library, Jest | @testing-library/svelte, Vitest |
| Prisma, Drizzle, SDK server Stripe | Sama, tanpa perubahan |
Patokan saya: apa pun yang tidak terikat framework (Zod, Prisma, Drizzle, date-fns, SDK server Stripe) pindah apa adanya. Apa pun yang ada kata "react" di namanya butuh pengganti. Cek library yang lebih spesifik (editor tertentu, SDK vendor, library chart) sebelum kamu memutuskan migrasi.
7. Caching: bagian yang tidak perlu kamu pusingkan lagi#
Next.js punya banyak lapisan cache. Next 16 membuatnya lebih sederhana dengan "use cache" yang opt-in, tapi kamu tetap harus memikirkan cache key, cacheLife, dan revalidateTag.
SvelteKit tidak punya data cache. load berjalan di setiap request. Kalau mau caching, kamu memakai alat bawaan web:
// +page.server.ts
export const load: PageServerLoad = async ({ setHeaders }) => {
setHeaders({ "cache-control": "public, max-age=60" });
return { posts: await getPosts() };
};| Next.js | SvelteKit |
|---|---|
| Halaman statis (SSG) | export const prerender = true |
export const dynamic = "force-dynamic" |
Default (SSR di setiap request) |
export const revalidate = 60 (ISR) |
Dengan adapter-vercel: export const config = { isr: { expiration: 60 } } |
| Halaman khusus client | export const ssr = false |
| Halaman tanpa JavaScript | export const csr = false |
Lebih sedikit sihir, lebih banyak kontrol. Buat saya, bagian ini terasa melegakan. 😌
8. Middleware: proxy.ts vs hooks.server.ts#
hooks.server.ts berjalan di setiap request. event.locals adalah tempat kamu menyimpan sesuatu (misalnya user yang sedang login) supaya bisa dibaca load dan actions nanti:
// src/hooks.server.ts
import { redirect, type Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = await getUser(event.cookies.get("session"));
if (event.url.pathname.startsWith("/dashboard") && !event.locals.user) {
redirect(303, "/login");
}
return resolve(event);
};Deklarasikan type locals di src/app.d.ts. Setelah itu load mana pun bisa membacanya: export const load = ({ locals }) => ({ user: locals.user }).
Satu lagi yang saya suka: SvelteKit tidak mau build kalau kamu meng-import dari $env/static/private di kode yang sampai ke browser. Next.js hanya memberimu undefined. Sangat sulit membocorkan secret secara tidak sengaja. 🔒
Jebakan di minggu pertama#
- Jangan simpan data request di state level modul. Di server,
$state(atau variabel apa pun) di level atas modul.svelte.tsdipakai bersama oleh semua user. Simpan data per user dievent.locals, oper lewatload, atau pakaisetContext. - Destructuring bisa memutus reaktivitas.
let { count } = someStatehanya menyalin nilainya sekali. BacasomeState.countlangsung, atau pakai$derived. - Pakai
$derived, bukan$effect, untuk nilai turunan.$effectuntuk side effect (DOM, timer, analytics). Kalau kamu mengubah state di dalam effect, kemungkinan besar yang kamu butuhkan adalah$derived. +page.tsjuga berjalan di browser. Jangan import database client di sana. Pakai+page.server.ts, atau simpan kode server disrc/lib/server/.- Pakai
fetchyang diberikan olehload.fetchini mengirim cookie, bisa memakai URL relatif di server, dan menghindari fetch dua kali saat hydration. - Runes hanya bekerja di file
.sveltedan.svelte.ts. Kalau file shared state kamu bernamastore.ts, ganti namanya jadistore.svelte.ts. tsctidak mengecek file.svelte. Jalankannpx sv checksebagai gantinya (atau sebagai tambahan).- Tutorial lama ada di mana-mana.
export let,$:,on:click, dan<slot />adalah Svelte 4. Masih jalan di legacy mode, tapi jangan dipelajari. Begitu juga library: pastikan sudah mendukung Svelte 5.
Cara saya melakukan migrasinya#
Next.js dan SvelteKit tidak bisa berjalan di aplikasi yang sama, jadi kamu tidak bisa migrasi komponen demi komponen seperti saat upgrade React. Bangun aplikasi baru di sebelah yang lama, mulai dari kode yang tidak ada hubungannya dengan React. Biasanya bagian itu lebih besar dari yang kamu kira. 🎉
Ini urutan yang akan saya ikuti:
- Kerjakan tutorial Svelte. Cuma satu sore, tapi menghemat berhari-hari.
- Buat daftar. Route, API route, middleware, env var, dan semua library yang ada kata "react" di namanya.
- Temukan penghambatnya. Library tanpa padanan yang bagus, parallel atau intercepting route. Putuskan soal ini sebelum menulis kode.
- Buat project baru dengan
npx sv create, lalunpx sv add tailwindcss eslint vitest. - Pindahkan kode yang tidak terikat framework (database, API client, schema Zod, utils, type) ke
src/lib/. - Siapkan UI kit (misalnya
npx shadcn-svelte@latest init) dan tambahkan komponen yang kamu pakai. - Bangun ulang root layout: nav, footer, tema, font,
<svelte:head>. - Pindahkan halaman, mulai dari yang paling sederhana. Setiap halaman
asyncmenjadi+page.server.ts(data) ++page.svelte(UI). - Pindahkan form dan mutasi. Server Actions jadi form actions, React Hook Form/Formik jadi Superforms,
route.tsjadi+server.ts. - Pindahkan middleware dan auth ke
hooks.server.ts, dan gantiNEXT_PUBLIC_*jadiPUBLIC_*. - Cek dan test.
npx sv checklolos, test E2E lolos, URL, redirect, sitemap, dan meta tag sama seperti sebelumnya. - Pilih adapter, deploy preview, lalu alihkan traffic. Per route kalau bisa: reverse proxy bisa mengirim
/blog/*ke aplikasi baru sementara aplikasi lama menangani sisanya.
Perlukah kamu migrasi?#
Pilih SvelteKit kalau:
- Kamu ingin bundle lebih kecil dan halaman lebih cepat tanpa banyak tuning.
- Caching Next.js dan pembagian server/client component terus membingungkan tim kamu.
- Kamu memulai project baru, atau aplikasinya cukup kecil untuk ditulis ulang.
- Kamu tidak ingin terikat ke satu platform hosting.
Tetap di Next.js kalau:
- Ada library penting di stack kamu yang tidak punya padanan Svelte yang bagus.
- Tim kamu besar dan merekrut developer React lebih mudah.
- Aplikasinya besar dan sudah berjalan dengan baik. Menulis ulang harus sepadan dengan hasilnya.
- Kamu mengandalkan parallel dan intercepting route.
Buat saya, bagian terbaik dari SvelteKit adalah betapa sedikit saya harus memikirkan framework-nya. Komponen adalah HTML dengan sedikit script. Data datang dari load. Form ya form. Caching ya HTTP. Setelah beberapa hari, saya berhenti mencari-cari dokumentasi dan langsung menulis kode.
Masih ragu? Tutorial resmi Svelte dari langkah 1 adalah cara tercepat untuk tahu apakah Svelte cocok buat kamu.
Selamat bermigrasi! 🧡
Referensi#
- Dokumentasi SvelteKit dan dokumentasi Svelte, resmi
- Routing, Loading data, dan Form actions di dokumentasi SvelteKit
- Remote functions, dokumentasi SvelteKit (eksperimental)
- Hooks, Page options, dan State management, dokumentasi SvelteKit
- What are runes? dan Svelte 5 migration guide, dokumentasi Svelte
- sv CLI, dokumentasi Svelte
- What's new in Svelte: June 2026, blog Svelte
- shadcn-svelte dan dokumentasi Form-nya (Formsnap + Superforms + Zod)
- Superforms dan Formsnap
- Bits UI dan Introducing Ark UI for Svelte
- TanStack Table untuk Svelte (v9, Svelte 5)
- Next.js 16, blog Next.js. Cache Components,
"use cache", danproxy.ts - Next.js 16: What's new, LogRocket
- Tutorial interaktif Svelte
