SvelteKit for React Developers — Key Concepts and Migration Guide

Sanjeev SharmaSanjeev Sharma
5 min read

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 function

Load 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:enhance on 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.svelte client components — use +page.server.ts for server-only code

Best Practices

  • Use +page.server.ts for database access and secrets — never expose them in client-side +page.ts
  • Use use:enhance on all forms to get progressive enhancement with JavaScript disabled
  • Use SvelteKit's $lib alias 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.svelte and +layout.svelte files, similar to Next.js App Router
  • Load functions in +page.server.ts are the equivalent of Next.js Server Component data fetching
  • Form actions in +page.server.ts handle 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:enhance progressively 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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading