that provide explicit, fine-grained reactivity.\n\n| Rune | Purpose | Replaces |\n| ----------- | --------------------- | --------------------------- |\n| `$state` | Reactive state | `let` declarations |\n| `$derived` | Computed values | `$:` reactive statements |\n| `$effect` | Side effects | `$:` side effect statements |\n| `$props` | Component props | `export let` |\n| `$bindable` | Two-way binding props | `export let` with bind: |\n\n### Basic Component Structure\n\n```svelte\n\u003cscript lang=\"ts\">\n // Props with TypeScript\n interface Props {\n title: string\n count?: number\n onUpdate?: (value: number) => void\n }\n\n let { title, count = 0, onUpdate }: Props = $props()\n\n // Reactive state\n let localCount = $state(count)\n let items = $state\u003cstring[]>([])\n\n // Derived values (auto-update when dependencies change)\n let doubled = $derived(localCount * 2)\n let total = $derived(items.reduce((sum, i) => sum + i.length, 0))\n\n // Side effects\n $effect(() => {\n console.log(`Count changed to: ${localCount}`)\n onUpdate?.(localCount)\n })\n\n // Methods\n function increment() {\n localCount++\n }\n\u003c/script>\n\n\u003cdiv class=\"component\">\n \u003ch1>{title}\u003c/h1>\n \u003cp>Count: {localCount} (Doubled: {doubled})\u003c/p>\n \u003cbutton onclick={increment}>Increment\u003c/button>\n\u003c/div>\n\n\u003cstyle>\n .component {\n padding: 1rem;\n }\n\u003c/style>\n```\n\n### State Patterns\n\n```typescript\n// Primitives\nlet count = $state(0);\nlet name = $state(\"\");\n\n// Objects (deeply reactive)\nlet user = $state({\n name: \"John\",\n email: \"john@example.com\",\n preferences: { theme: \"dark\" },\n});\n\n// Arrays\nlet items = $state\u003cItem[]>([]);\n\n// Direct mutation works!\nuser.name = \"Jane\"; // Reactive\nuser.preferences.theme = \"light\"; // Reactive (deep)\nitems.push({ id: 1, name: \"New\" }); // Reactive\n\n// Frozen state (shallow reactivity)\nlet frozenList = $state.frozen([1, 2, 3]);\n```\n\n### Derived Values\n\n```typescript\n// Simple derived\nlet doubled = $derived(count * 2);\n\n// Complex derived (use $derived.by for multi-line)\nlet stats = $derived.by(() => {\n const total = items.reduce((sum, i) => sum + i.value, 0);\n const average = items.length ? total / items.length : 0;\n return { total, average, count: items.length };\n});\n\n// Derived from multiple sources\nlet summary = $derived(`${user.name} has ${items.length} items`);\n```\n\n### Effects\n\n```typescript\n// Basic effect (runs when dependencies change)\n$effect(() => {\n console.log(`Count is now: ${count}`);\n});\n\n// Effect with cleanup\n$effect(() => {\n const interval = setInterval(() => {\n count++;\n }, 1000);\n\n // Cleanup function (returned)\n return () => clearInterval(interval);\n});\n\n// Pre-effect (runs before DOM updates)\n$effect.pre(() => {\n console.log(\"About to update DOM\");\n});\n\n// Root effect (doesn't track dependencies)\n$effect.root(() => {\n // Manual dependency management\n});\n```\n\n---\n\n## Component Patterns\n\n### Props with Defaults and Spreading\n\n```svelte\n\u003cscript lang=\"ts\">\n interface Props {\n variant?: 'primary' | 'secondary'\n size?: 'sm' | 'md' | 'lg'\n disabled?: boolean\n children: import('svelte').Snippet\n }\n\n let {\n variant = 'primary',\n size = 'md',\n disabled = false,\n children,\n ...restProps\n }: Props = $props()\n\u003c/script>\n\n\u003cbutton\n class=\"btn btn-{variant} btn-{size}\"\n {disabled}\n {...restProps}\n>\n {@render children()}\n\u003c/button>\n```\n\n### Two-Way Binding with $bindable\n\n```svelte\n\u003c!-- Input.svelte -->\n\u003cscript lang=\"ts\">\n interface Props {\n value: string\n }\n\n let { value = $bindable() }: Props = $props()\n\u003c/script>\n\n\u003cinput bind:value />\n\n\u003c!-- Parent.svelte -->\n\u003cscript lang=\"ts\">\n let name = $state('')\n\u003c/script>\n\n\u003cInput bind:value={name} />\n\u003cp>Hello, {name}!\u003c/p>\n```\n\n### Snippets (Replacing Slots)\n\n```svelte\n\u003c!-- Card.svelte -->\n\u003cscript lang=\"ts\">\n import type { Snippet } from 'svelte'\n\n interface Props {\n title: string\n children: Snippet\n footer?: Snippet\n }\n\n let { title, children, footer }: Props = $props()\n\u003c/script>\n\n\u003cdiv class=\"card\">\n \u003cheader>{title}\u003c/header>\n \u003cmain>{@render children()}\u003c/main>\n {#if footer}\n \u003cfooter>{@render footer()}\u003c/footer>\n {/if}\n\u003c/div>\n\n\u003c!-- Usage -->\n\u003cCard title=\"My Card\">\n \u003cp>Card content goes here\u003c/p>\n\n {#snippet footer()}\n \u003cbutton>Action\u003c/button>\n {/snippet}\n\u003c/Card>\n```\n\n### Snippets with Parameters\n\n```svelte\n\u003c!-- List.svelte -->\n\u003cscript lang=\"ts\" generics=\"T\">\n import type { Snippet } from 'svelte'\n\n interface Props {\n items: T[]\n row: Snippet\u003c[T, number]>\n empty?: Snippet\n }\n\n let { items, row, empty }: Props = $props()\n\u003c/script>\n\n{#if items.length === 0}\n {#if empty}\n {@render empty()}\n {:else}\n \u003cp>No items\u003c/p>\n {/if}\n{:else}\n \u003cul>\n {#each items as item, index}\n \u003cli>{@render row(item, index)}\u003c/li>\n {/each}\n \u003c/ul>\n{/if}\n\n\u003c!-- Usage -->\n\u003cList items={users}>\n {#snippet row(user, i)}\n \u003cspan>{i + 1}. {user.name}\u003c/span>\n {/snippet}\n\n {#snippet empty()}\n \u003cp>No users found\u003c/p>\n {/snippet}\n\u003c/List>\n```\n\n---\n\n## Shared State with .svelte.ts Files\n\n### Creating Shared State\n\n```typescript\n// lib/stores/counter.svelte.ts\nexport function createCounter(initial = 0) {\n let count = $state(initial);\n\n return {\n get count() {\n return count;\n },\n increment() {\n count++;\n },\n decrement() {\n count--;\n },\n reset() {\n count = initial;\n },\n };\n}\n\n// Singleton instance\nexport const counter = createCounter();\n```\n\n### Class-Based State\n\n```typescript\n// lib/stores/user.svelte.ts\nexport class UserStore {\n user = $state\u003cUser | null>(null);\n loading = $state(false);\n error = $state\u003cstring | null>(null);\n\n isLoggedIn = $derived(!!this.user);\n\n async login(email: string, password: string) {\n this.loading = true;\n this.error = null;\n\n try {\n const response = await fetch(\"/api/auth/login\", {\n method: \"POST\",\n body: JSON.stringify({ email, password }),\n headers: { \"Content-Type\": \"application/json\" },\n });\n\n if (!response.ok) throw new Error(\"Login failed\");\n\n this.user = await response.json();\n } catch (e) {\n this.error = e instanceof Error ? e.message : \"Unknown error\";\n throw e;\n } finally {\n this.loading = false;\n }\n }\n\n logout() {\n this.user = null;\n }\n}\n\nexport const userStore = new UserStore();\n```\n\n### Using Shared State\n\n```svelte\n\u003cscript lang=\"ts\">\n import { counter } from '$lib/stores/counter.svelte'\n import { userStore } from '$lib/stores/user.svelte'\n\u003c/script>\n\n\u003cp>Count: {counter.count}\u003c/p>\n\u003cbutton onclick={counter.increment}>+\u003c/button>\n\n{#if userStore.isLoggedIn}\n \u003cp>Welcome, {userStore.user?.name}\u003c/p>\n \u003cbutton onclick={() => userStore.logout()}>Logout\u003c/button>\n{:else}\n \u003cbutton onclick={() => userStore.login('test@test.com', 'pass')}>\n Login\n \u003c/button>\n{/if}\n```\n\n---\n\n## SvelteKit (Full-Stack)\n\n### Project Structure\n\n```\nsveltekit-app/\n├── src/\n│ ├── routes/ # File-based routing\n│ │ ├── +page.svelte # /\n│ │ ├── +page.server.ts # Server load function\n│ │ ├── +layout.svelte # Root layout\n│ │ ├── about/\n│ │ │ └── +page.svelte # /about\n│ │ ├── users/\n│ │ │ ├── +page.svelte # /users\n│ │ │ └── [id]/\n│ │ │ ├── +page.svelte # /users/:id\n│ │ │ └── +page.server.ts\n│ │ └── api/\n│ │ └── users/\n│ │ └── +server.ts # /api/users\n│ ├── lib/ # $lib alias\n│ │ ├── components/\n│ │ └── stores/\n│ └── app.html\n├── static/\n└── svelte.config.js\n```\n\n### Load Functions\n\n```typescript\n// routes/users/+page.server.ts\nimport type { PageServerLoad } from './$types'\nimport { error } from '@sveltejs/kit'\n\nexport const load: PageServerLoad = async ({ fetch, params }) => {\n const response = await fetch('/api/users')\n\n if (!response.ok) {\n throw error(response.status, 'Failed to load users')\n }\n\n const users = await response.json()\n\n return { users }\n}\n\n// routes/users/+page.svelte\n\u003cscript lang=\"ts\">\n import type { PageData } from './$types'\n\n let { data }: { data: PageData } = $props()\n\u003c/script>\n\n\u003ch1>Users\u003c/h1>\n{#each data.users as user}\n \u003cp>{user.name}\u003c/p>\n{/each}\n```\n\n### Form Actions\n\n```typescript\n// routes/login/+page.server.ts\nimport type { Actions } from \"./$types\";\nimport { fail, redirect } from \"@sveltejs/kit\";\n\nexport const actions: Actions = {\n default: async ({ request, cookies }) => {\n const formData = await request.formData();\n const email = formData.get(\"email\") as string;\n const password = formData.get(\"password\") as string;\n\n // Validation\n if (!email || !password) {\n return fail(400, { email, missing: true });\n }\n\n // Authentication\n const user = await authenticate(email, password);\n\n if (!user) {\n return fail(401, { email, incorrect: true });\n }\n\n // Set session cookie\n cookies.set(\"session\", user.token, { path: \"/\" });\n\n throw redirect(303, \"/dashboard\");\n },\n};\n```\n\n```svelte\n\u003c!-- routes/login/+page.svelte -->\n\u003cscript lang=\"ts\">\n import type { ActionData } from './$types'\n import { enhance } from '$app/forms'\n\n let { form }: { form: ActionData } = $props()\n\u003c/script>\n\n\u003cform method=\"POST\" use:enhance>\n \u003cinput name=\"email\" value={form?.email ?? ''} />\n\n {#if form?.missing}\n \u003cp class=\"error\">All fields are required\u003c/p>\n {/if}\n\n {#if form?.incorrect}\n \u003cp class=\"error\">Invalid credentials\u003c/p>\n {/if}\n\n \u003cinput name=\"password\" type=\"password\" />\n \u003cbutton>Login\u003c/button>\n\u003c/form>\n```\n\n### API Routes\n\n```typescript\n// routes/api/users/+server.ts\nimport type { RequestHandler } from \"./$types\";\nimport { json, error } from \"@sveltejs/kit\";\n\nexport const GET: RequestHandler = async ({ url }) => {\n const limit = Number(url.searchParams.get(\"limit\")) || 10;\n\n const users = await prisma.user.findMany({ take: limit });\n\n return json(users);\n};\n\nexport const POST: RequestHandler = async ({ request }) => {\n const body = await request.json();\n\n if (!body.email || !body.name) {\n throw error(400, \"Missing required fields\");\n }\n\n const user = await prisma.user.create({ data: body });\n\n return json(user, { status: 201 });\n};\n\n// routes/api/users/[id]/+server.ts\nexport const GET: RequestHandler = async ({ params }) => {\n const user = await prisma.user.findUnique({\n where: { id: params.id },\n });\n\n if (!user) {\n throw error(404, \"User not found\");\n }\n\n return json(user);\n};\n```\n\n### Middleware (Hooks)\n\n```typescript\n// src/hooks.server.ts\nimport type { Handle } from \"@sveltejs/kit\";\n\nexport const handle: Handle = async ({ event, resolve }) => {\n // Get session from cookie\n const session = event.cookies.get(\"session\");\n\n if (session) {\n const user = await validateSession(session);\n event.locals.user = user;\n }\n\n // Protected routes\n if (event.url.pathname.startsWith(\"/dashboard\") && !event.locals.user) {\n return new Response(\"Redirect\", {\n status: 303,\n headers: { Location: \"/login\" },\n });\n }\n\n return resolve(event);\n};\n```\n\n---\n\n## Event Handling (Svelte 5)\n\n### DOM Events (New Syntax)\n\n```svelte\n\u003c!-- Old: on:click -->\n\u003c!-- New: onclick -->\n\n\u003cbutton onclick={() => count++}>Click me\u003c/button>\n\n\u003cinput\n oninput={(e) => name = e.currentTarget.value}\n onkeydown={(e) => e.key === 'Enter' && submit()}\n/>\n\n\u003c!-- Event modifiers: use JavaScript -->\n\u003cbutton onclick={(e) => {\n e.preventDefault()\n e.stopPropagation()\n handleClick()\n}}>\n Submit\n\u003c/button>\n\n\u003c!-- Once: use wrapper -->\n\u003cscript>\n function once(fn) {\n return function(e) {\n if (!e.target.dataset.clicked) {\n e.target.dataset.clicked = 'true'\n fn(e)\n }\n }\n }\n\u003c/script>\n\n\u003cbutton onclick={once(() => console.log('Once!'))}>\n Click once\n\u003c/button>\n```\n\n### Component Events (Callback Props)\n\n```svelte\n\u003c!-- Old: createEventDispatcher -->\n\u003c!-- New: callback props -->\n\n\u003c!-- Child.svelte -->\n\u003cscript lang=\"ts\">\n interface Props {\n onSelect?: (id: string) => void\n onClose?: () => void\n }\n\n let { onSelect, onClose }: Props = $props()\n\u003c/script>\n\n\u003cbutton onclick={() => onSelect?.('123')}>Select\u003c/button>\n\u003cbutton onclick={onClose}>Close\u003c/button>\n\n\u003c!-- Parent.svelte -->\n\u003cChild\n onSelect={(id) => console.log('Selected:', id)}\n onClose={() => console.log('Closed')}\n/>\n```\n\n---\n\n## Testing with Vitest\n\n### Setup\n\n```typescript\n// vitest.config.ts\nimport { defineConfig } from \"vitest/config\";\nimport { svelte } from \"@sveltejs/vite-plugin-svelte\";\n\nexport default defineConfig({\n plugins: [svelte({ hot: !process.env.VITEST })],\n test: {\n globals: true,\n environment: \"jsdom\",\n include: [\"src/**/*.{test,spec}.{js,ts}\"],\n },\n});\n```\n\n### Component Testing\n\n```typescript\n// tests/Counter.test.ts\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen, fireEvent } from \"@testing-library/svelte\";\nimport Counter from \"$lib/components/Counter.svelte\";\n\ndescribe(\"Counter\", () => {\n it(\"renders initial count\", () => {\n render(Counter, { props: { initial: 5 } });\n\n expect(screen.getByText(\"Count: 5\")).toBeInTheDocument();\n });\n\n it(\"increments on click\", async () => {\n render(Counter);\n\n const button = screen.getByRole(\"button\", { name: /increment/i });\n await fireEvent.click(button);\n\n expect(screen.getByText(\"Count: 1\")).toBeInTheDocument();\n });\n});\n```\n\n### Testing Stores\n\n```typescript\n// tests/stores/counter.test.ts\nimport { describe, it, expect } from \"vitest\";\nimport { counter } from \"$lib/stores/counter.svelte\";\n\ndescribe(\"counter store\", () => {\n it(\"increments count\", () => {\n counter.reset();\n expect(counter.count).toBe(0);\n\n counter.increment();\n expect(counter.count).toBe(1);\n\n counter.increment();\n expect(counter.count).toBe(2);\n });\n\n it(\"decrements count\", () => {\n counter.reset();\n counter.decrement();\n expect(counter.count).toBe(-1);\n });\n});\n```\n\n---\n\n## Migration from Svelte 4\n\n| Svelte 4 | Svelte 5 |\n| -------------------------- | ----------------------------------- |\n| `let count = 0` (reactive) | `let count = $state(0)` |\n| `$: doubled = count * 2` | `let doubled = $derived(count * 2)` |\n| `$: console.log(count)` | `$effect(() => console.log(count))` |\n| `export let value` | `let { value } = $props()` |\n| `on:click={handler}` | `onclick={handler}` |\n| `\u003cslot />` | `{@render children()}` |\n| `createEventDispatcher()` | Callback props |\n\n### Migration Script\n\n```bash\nnpx sv migrate svelte-5\n```\n\n---\n\n## Anti-Patterns to Avoid\n\n| Anti-Pattern | Problem | Solution |\n| ---------------------------------- | ---------------------- | --------------------------- |\n| Using `$effect` for derived values | Unnecessary complexity | Use `$derived` |\n| Exporting `$state` directly | Breaks reactivity | Export getter/setter object |\n| Not using TypeScript | Missing type safety | Enable `lang=\"ts\"` |\n| Using old slot syntax | Deprecated in Svelte 5 | Use snippets |\n| Using `on:event` syntax | Deprecated in Svelte 5 | Use `onevent` props |\n\n---\n\n## Performance Benefits\n\n| Metric | Improvement |\n| ------------------- | ----------------------------- |\n| Bundle size | 40-60% smaller than React/Vue |\n| Runtime performance | No virtual DOM overhead |\n| Time to interactive | Minimal JavaScript hydration |\n| Memory usage | Lower due to compiled output |\n\n---\n\n## Related Resources\n\n- [Svelte 5 Documentation](https://svelte.dev/docs)\n- [SvelteKit Documentation](https://svelte.dev/docs/kit)\n- [Svelte 5 Migration Guide](https://svelte.dev/docs/svelte/v5-migration-guide)\n- [Svelte Testing Docs](https://svelte.dev/docs/svelte/testing)\n- [Joy of Code Tutorials](https://joyofcode.xyz/)\n\n---\n\n## When to Use This Skill\n\n- Building new Svelte 5 applications with runes\n- Migrating from Svelte 4 to Svelte 5\n- Full-stack development with SvelteKit\n- Creating reactive shared state\n- Implementing form actions and API routes\n- Testing Svelte components with Vitest\n","licenseDocuments":[]}}
L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Développement · SKILL.md

svelte-development

svelte-development

Svelte 5 development with runes ($state, $derived, $effect), SvelteKit full-stack framework, and modern reactive patterns. Use when building Svelte applications, implementing fine-grained reactivity, or working with SvelteKit routing and server functions.

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: svelte-development description: Svelte 5 development with runes ($state, $derived, $effect), SvelteKit full-stack framework, and modern reactive patterns. Use when building Svelte applications, implementing fine-grained reactivity, or working with SvelteKit routing and server functions.

Svelte 5 Development

Comprehensive guide for building modern Svelte applications with runes and SvelteKit.

Stack Overview

ToolPurposeVersion
Svelte 5Core framework5.0+
SvelteKitFull-stack framework2.0+
TypeScriptType safety5.0+
ViteBuild tool5.0+
VitestTesting1.0+

Svelte 5 Runes (Core Reactivity)

The Rune System

Runes are compile-time macros prefixed with $ that provide explicit, fine-grained reactivity.

RunePurposeReplaces
$stateReactive statelet declarations
$derivedComputed values$: reactive statements
$effectSide effects$: side effect statements
$propsComponent propsexport let
$bindableTwo-way binding propsexport let with bind:

Basic Component Structure

<script lang="ts">
  // Props with TypeScript
  interface Props {
    title: string
    count?: number
    onUpdate?: (value: number) => void
  }

  let { title, count = 0, onUpdate }: Props = $props()

  // Reactive state
  let localCount = $state(count)
  let items = $state<string[]>([])

  // Derived values (auto-update when dependencies change)
  let doubled = $derived(localCount * 2)
  let total = $derived(items.reduce((sum, i) => sum + i.length, 0))

  // Side effects
  $effect(() => {
    console.log(`Count changed to: ${localCount}`)
    onUpdate?.(localCount)
  })

  // Methods
  function increment() {
    localCount++
  }
</script>

<div class="component">
  <h1>{title}</h1>
  <p>Count: {localCount} (Doubled: {doubled})</p>
  <button onclick={increment}>Increment</button>
</div>

<style>
  .component {
    padding: 1rem;
  }
</style>

State Patterns

// Primitives
let count = $state(0);
let name = $state("");

// Objects (deeply reactive)
let user = $state({
  name: "John",
  email: "john@example.com",
  preferences: { theme: "dark" },
});

// Arrays
let items = $state<Item[]>([]);

// Direct mutation works!
user.name = "Jane"; // Reactive
user.preferences.theme = "light"; // Reactive (deep)
items.push({ id: 1, name: "New" }); // Reactive

// Frozen state (shallow reactivity)
let frozenList = $state.frozen([1, 2, 3]);

Derived Values

// Simple derived
let doubled = $derived(count * 2);

// Complex derived (use $derived.by for multi-line)
let stats = $derived.by(() => {
  const total = items.reduce((sum, i) => sum + i.value, 0);
  const average = items.length ? total / items.length : 0;
  return { total, average, count: items.length };
});

// Derived from multiple sources
let summary = $derived(`${user.name} has ${items.length} items`);

Effects

// Basic effect (runs when dependencies change)
$effect(() => {
  console.log(`Count is now: ${count}`);
});

// Effect with cleanup
$effect(() => {
  const interval = setInterval(() => {
    count++;
  }, 1000);

  // Cleanup function (returned)
  return () => clearInterval(interval);
});

// Pre-effect (runs before DOM updates)
$effect.pre(() => {
  console.log("About to update DOM");
});

// Root effect (doesn't track dependencies)
$effect.root(() => {
  // Manual dependency management
});

Component Patterns

Props with Defaults and Spreading

<script lang="ts">
  interface Props {
    variant?: 'primary' | 'secondary'
    size?: 'sm' | 'md' | 'lg'
    disabled?: boolean
    children: import('svelte').Snippet
  }

  let {
    variant = 'primary',
    size = 'md',
    disabled = false,
    children,
    ...restProps
  }: Props = $props()
</script>

<button
  class="btn btn-{variant} btn-{size}"
  {disabled}
  {...restProps}
>
  {@render children()}
</button>

Two-Way Binding with $bindable

<!-- Input.svelte -->
<script lang="ts">
  interface Props {
    value: string
  }

  let { value = $bindable() }: Props = $props()
</script>

<input bind:value />

<!-- Parent.svelte -->
<script lang="ts">
  let name = $state('')
</script>

<Input bind:value={name} />
<p>Hello, {name}!</p>

Snippets (Replacing Slots)

<!-- Card.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte'

  interface Props {
    title: string
    children: Snippet
    footer?: Snippet
  }

  let { title, children, footer }: Props = $props()
</script>

<div class="card">
  <header>{title}</header>
  <main>{@render children()}</main>
  {#if footer}
    <footer>{@render footer()}</footer>
  {/if}
</div>

<!-- Usage -->
<Card title="My Card">
  <p>Card content goes here</p>

  {#snippet footer()}
    <button>Action</button>
  {/snippet}
</Card>

Snippets with Parameters

<!-- List.svelte -->
<script lang="ts" generics="T">
  import type { Snippet } from 'svelte'

  interface Props {
    items: T[]
    row: Snippet<[T, number]>
    empty?: Snippet
  }

  let { items, row, empty }: Props = $props()
</script>

{#if items.length === 0}
  {#if empty}
    {@render empty()}
  {:else}
    <p>No items</p>
  {/if}
{:else}
  <ul>
    {#each items as item, index}
      <li>{@render row(item, index)}</li>
    {/each}
  </ul>
{/if}

<!-- Usage -->
<List items={users}>
  {#snippet row(user, i)}
    <span>{i + 1}. {user.name}</span>
  {/snippet}

  {#snippet empty()}
    <p>No users found</p>
  {/snippet}
</List>

Shared State with .svelte.ts Files

Creating Shared State

// lib/stores/counter.svelte.ts
export function createCounter(initial = 0) {
  let count = $state(initial);

  return {
    get count() {
      return count;
    },
    increment() {
      count++;
    },
    decrement() {
      count--;
    },
    reset() {
      count = initial;
    },
  };
}

// Singleton instance
export const counter = createCounter();

Class-Based State

// lib/stores/user.svelte.ts
export class UserStore {
  user = $state<User | null>(null);
  loading = $state(false);
  error = $state<string | null>(null);

  isLoggedIn = $derived(!!this.user);

  async login(email: string, password: string) {
    this.loading = true;
    this.error = null;

    try {
      const response = await fetch("/api/auth/login", {
        method: "POST",
        body: JSON.stringify({ email, password }),
        headers: { "Content-Type": "application/json" },
      });

      if (!response.ok) throw new Error("Login failed");

      this.user = await response.json();
    } catch (e) {
      this.error = e instanceof Error ? e.message : "Unknown error";
      throw e;
    } finally {
      this.loading = false;
    }
  }

  logout() {
    this.user = null;
  }
}

export const userStore = new UserStore();

Using Shared State

<script lang="ts">
  import { counter } from '$lib/stores/counter.svelte'
  import { userStore } from '$lib/stores/user.svelte'
</script>

<p>Count: {counter.count}</p>
<button onclick={counter.increment}>+</button>

{#if userStore.isLoggedIn}
  <p>Welcome, {userStore.user?.name}</p>
  <button onclick={() => userStore.logout()}>Logout</button>
{:else}
  <button onclick={() => userStore.login('test@test.com', 'pass')}>
    Login
  </button>
{/if}

SvelteKit (Full-Stack)

Project Structure

sveltekit-app/
├── src/
│   ├── routes/              # File-based routing
│   │   ├── +page.svelte     # /
│   │   ├── +page.server.ts  # Server load function
│   │   ├── +layout.svelte   # Root layout
│   │   ├── about/
│   │   │   └── +page.svelte # /about
│   │   ├── users/
│   │   │   ├── +page.svelte # /users
│   │   │   └── [id]/
│   │   │       ├── +page.svelte      # /users/:id
│   │   │       └── +page.server.ts
│   │   └── api/
│   │       └── users/
│   │           └── +server.ts  # /api/users
│   ├── lib/                 # $lib alias
│   │   ├── components/
│   │   └── stores/
│   └── app.html
├── static/
└── svelte.config.js

Load Functions

// routes/users/+page.server.ts
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'

export const load: PageServerLoad = async ({ fetch, params }) => {
  const response = await fetch('/api/users')

  if (!response.ok) {
    throw error(response.status, 'Failed to load users')
  }

  const users = await response.json()

  return { users }
}

// routes/users/+page.svelte
<script lang="ts">
  import type { PageData } from './$types'

  let { data }: { data: PageData } = $props()
</script>

<h1>Users</h1>
{#each data.users as user}
  <p>{user.name}</p>
{/each}

Form Actions

// routes/login/+page.server.ts
import type { Actions } from "./$types";
import { fail, redirect } from "@sveltejs/kit";

export const actions: Actions = {
  default: async ({ request, cookies }) => {
    const formData = await request.formData();
    const email = formData.get("email") as string;
    const password = formData.get("password") as string;

    // Validation
    if (!email || !password) {
      return fail(400, { email, missing: true });
    }

    // Authentication
    const user = await authenticate(email, password);

    if (!user) {
      return fail(401, { email, incorrect: true });
    }

    // Set session cookie
    cookies.set("session", user.token, { path: "/" });

    throw redirect(303, "/dashboard");
  },
};
<!-- routes/login/+page.svelte -->
<script lang="ts">
  import type { ActionData } from './$types'
  import { enhance } from '$app/forms'

  let { form }: { form: ActionData } = $props()
</script>

<form method="POST" use:enhance>
  <input name="email" value={form?.email ?? ''} />

  {#if form?.missing}
    <p class="error">All fields are required</p>
  {/if}

  {#if form?.incorrect}
    <p class="error">Invalid credentials</p>
  {/if}

  <input name="password" type="password" />
  <button>Login</button>
</form>

API Routes

// routes/api/users/+server.ts
import type { RequestHandler } from "./$types";
import { json, error } from "@sveltejs/kit";

export const GET: RequestHandler = async ({ url }) => {
  const limit = Number(url.searchParams.get("limit")) || 10;

  const users = await prisma.user.findMany({ take: limit });

  return json(users);
};

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();

  if (!body.email || !body.name) {
    throw error(400, "Missing required fields");
  }

  const user = await prisma.user.create({ data: body });

  return json(user, { status: 201 });
};

// routes/api/users/[id]/+server.ts
export const GET: RequestHandler = async ({ params }) => {
  const user = await prisma.user.findUnique({
    where: { id: params.id },
  });

  if (!user) {
    throw error(404, "User not found");
  }

  return json(user);
};

Middleware (Hooks)

// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";

export const handle: Handle = async ({ event, resolve }) => {
  // Get session from cookie
  const session = event.cookies.get("session");

  if (session) {
    const user = await validateSession(session);
    event.locals.user = user;
  }

  // Protected routes
  if (event.url.pathname.startsWith("/dashboard") && !event.locals.user) {
    return new Response("Redirect", {
      status: 303,
      headers: { Location: "/login" },
    });
  }

  return resolve(event);
};

Event Handling (Svelte 5)

DOM Events (New Syntax)

<!-- Old: on:click -->
<!-- New: onclick -->

<button onclick={() => count++}>Click me</button>

<input
  oninput={(e) => name = e.currentTarget.value}
  onkeydown={(e) => e.key === 'Enter' && submit()}
/>

<!-- Event modifiers: use JavaScript -->
<button onclick={(e) => {
  e.preventDefault()
  e.stopPropagation()
  handleClick()
}}>
  Submit
</button>

<!-- Once: use wrapper -->
<script>
  function once(fn) {
    return function(e) {
      if (!e.target.dataset.clicked) {
        e.target.dataset.clicked = 'true'
        fn(e)
      }
    }
  }
</script>

<button onclick={once(() => console.log('Once!'))}>
  Click once
</button>

Component Events (Callback Props)

<!-- Old: createEventDispatcher -->
<!-- New: callback props -->

<!-- Child.svelte -->
<script lang="ts">
  interface Props {
    onSelect?: (id: string) => void
    onClose?: () => void
  }

  let { onSelect, onClose }: Props = $props()
</script>

<button onclick={() => onSelect?.('123')}>Select</button>
<button onclick={onClose}>Close</button>

<!-- Parent.svelte -->
<Child
  onSelect={(id) => console.log('Selected:', id)}
  onClose={() => console.log('Closed')}
/>

Testing with Vitest

Setup

// vitest.config.ts
import { defineConfig } from "vitest/config";
import { svelte } from "@sveltejs/vite-plugin-svelte";

export default defineConfig({
  plugins: [svelte({ hot: !process.env.VITEST })],
  test: {
    globals: true,
    environment: "jsdom",
    include: ["src/**/*.{test,spec}.{js,ts}"],
  },
});

Component Testing

// tests/Counter.test.ts
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@testing-library/svelte";
import Counter from "$lib/components/Counter.svelte";

describe("Counter", () => {
  it("renders initial count", () => {
    render(Counter, { props: { initial: 5 } });

    expect(screen.getByText("Count: 5")).toBeInTheDocument();
  });

  it("increments on click", async () => {
    render(Counter);

    const button = screen.getByRole("button", { name: /increment/i });
    await fireEvent.click(button);

    expect(screen.getByText("Count: 1")).toBeInTheDocument();
  });
});

Testing Stores

// tests/stores/counter.test.ts
import { describe, it, expect } from "vitest";
import { counter } from "$lib/stores/counter.svelte";

describe("counter store", () => {
  it("increments count", () => {
    counter.reset();
    expect(counter.count).toBe(0);

    counter.increment();
    expect(counter.count).toBe(1);

    counter.increment();
    expect(counter.count).toBe(2);
  });

  it("decrements count", () => {
    counter.reset();
    counter.decrement();
    expect(counter.count).toBe(-1);
  });
});

Migration from Svelte 4

Svelte 4Svelte 5
let count = 0 (reactive)let count = $state(0)
$: doubled = count * 2let doubled = $derived(count * 2)
$: console.log(count)$effect(() => console.log(count))
export let valuelet { value } = $props()
on:click={handler}onclick={handler}
<slot />{@render children()}
createEventDispatcher()Callback props

Migration Script

npx sv migrate svelte-5

Anti-Patterns to Avoid

Anti-PatternProblemSolution
Using $effect for derived valuesUnnecessary complexityUse $derived
Exporting $state directlyBreaks reactivityExport getter/setter object
Not using TypeScriptMissing type safetyEnable lang="ts"
Using old slot syntaxDeprecated in Svelte 5Use snippets
Using on:event syntaxDeprecated in Svelte 5Use onevent props

Performance Benefits

MetricImprovement
Bundle size40-60% smaller than React/Vue
Runtime performanceNo virtual DOM overhead
Time to interactiveMinimal JavaScript hydration
Memory usageLower due to compiled output

Related Resources


When to Use This Skill

  • Building new Svelte 5 applications with runes
  • Migrating from Svelte 4 to Svelte 5
  • Full-stack development with SvelteKit
  • Creating reactive shared state
  • Implementing form actions and API routes
  • Testing Svelte components with Vitest
Skills enregistrées