SvelteKit for React Developers — Key Concepts and Migration Guide
Advertisement
Introduction
Why This Matters
SvelteKit compiles away the framework at build time — no virtual DOM, no runtime overhead. For performance-sensitive applications or developers who prefer less boilerplate, it is a compelling choice. Understanding SvelteKit also deepens your understanding of what React and Next.js do under the hood.
Svelte vs React: Mental Model
In React, you write JavaScript that returns JSX. In Svelte, you write single-file components with <script>, <template> (implicit), and <style> sections.
React:
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
)
}Svelte:
<script>
let count = 0
function increment() { count++ }
</script>
<p>Count: {count}</p>
<button on:click={increment}>Increment</button>Svelte's compiler tracks which variables are reactive and generates minimal, targeted DOM updates. No virtual DOM diffing.
Routing in SvelteKit
SvelteKit uses file-system routing similar to Next.js App Router:
src/routes/
+page.svelte → /
+layout.svelte → Root layout
blog/
+page.svelte → /blog
+layout.svelte → Blog layout
[slug]/
+page.svelte → /blog/[slug]
+page.server.ts → Server-only load functionLoad Functions — Equivalent to Next.js Data Fetching
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params, locals }) => {
const post = await locals.db.posts.findUnique({ where: { slug: params.slug } })
if (!post) {
throw error(404, { message: 'Post not found' })
}
return { post } // Available as `data` in the component
}<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types'
export let data: PageData
</script>
<article>
<h1>{data.post.title}</h1>
<p>{data.post.excerpt}</p>
<div>{@html data.post.content}</div>
</article>Form Actions — Equivalent to Next.js Server Actions
// src/routes/contact/+page.server.ts
import type { Actions } from './$types'
import { fail, redirect } from '@sveltejs/kit'
export const actions: Actions = {
default: async ({ request, locals }) => {
const data = await request.formData()
const email = data.get('email') as string
const message = data.get('message') as string
if (!email || !message) {
return fail(400, { error: 'All fields are required', email, message })
}
await locals.emailService.send({ to: email, body: message })
throw redirect(303, '/contact/success')
}
}<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms'
import type { ActionData } from './$types'
export let form: ActionData
</script>
<!-- enhance progressively enhances the native form with JavaScript -->
<form method="POST" use:enhance>
<input name="email" type="email" required />
<textarea name="message" required />
{#if form?.error}
<p class="text-red-600">{form.error}</p>
{/if}
<button type="submit">Send</button>
</form>Svelte Stores — Equivalent to React State/Context
// stores/cart.ts
import { writable, derived } from 'svelte/store'
type CartItem = { id: string; name: string; price: number; quantity: number }
function createCartStore() {
const { subscribe, update, set } = writable<CartItem[]>([])
return {
subscribe,
addItem: (item: CartItem) => update(items => {
const existing = items.find(i => i.id === item.id)
if (existing) return items.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i)
return [...items, { ...item, quantity: 1 }]
}),
removeItem: (id: string) => update(items => items.filter(i => i.id !== id)),
clear: () => set([])
}
}
export const cart = createCartStore()
export const cartTotal = derived(cart, $cart => $cart.reduce((sum, i) => sum + i.price * i.quantity, 0))<script>
import { cart, cartTotal } from '$lib/stores/cart'
</script>
<p>Items: {$cart.length}</p>
<p>Total: ${$cartTotal.toFixed(2)}</p>
<button on:click={() => cart.clear()}>Clear Cart</button>The $ prefix auto-subscribes and auto-unsubscribes from the store.
Common Mistakes
- Trying to use React hooks in Svelte — Svelte has its own reactivity system (stores,
$:reactive statements) - Forgetting
use:enhanceon forms — without it, form actions work but reload the whole page - Mixing Svelte reactive syntax with React mental models — they are fundamentally different
- Importing Node.js modules in
+page.svelteclient components — use+page.server.tsfor server-only code
Best Practices
- Use
+page.server.tsfor database access and secrets — never expose them in client-side+page.ts - Use
use:enhanceon all forms to get progressive enhancement with JavaScript disabled - Use SvelteKit's
$libalias for shared code rather than relative imports - Prefer Svelte stores over prop drilling for shared state across components
Key Takeaways
- Svelte compiles reactivity away at build time — no virtual DOM, no runtime diffing, smaller bundles
- SvelteKit's file-system routing uses
+page.svelteand+layout.sveltefiles, similar to Next.js App Router - Load functions in
+page.server.tsare the equivalent of Next.js Server Component data fetching - Form actions in
+page.server.tshandle POST requests — equivalent to Next.js Server Actions - Svelte stores (
writable,derived) replace React Context and Zustand for global state - The
$prefix in Svelte templates auto-subscribes to a store and triggers reactive updates use:enhanceprogressively enhances native HTML forms with JavaScript for a SPA-like experience- SvelteKit cannot use React component libraries — you are committed to the Svelte ecosystem when you choose it
Advertisement