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":[]}}
La interfaz está localizada; las descripciones y documentos de las Skills se traducen automáticamente con IA a su idioma. El código, los comandos, rutas y términos técnicos se mantienen en el original, y prevalece el documento del autor.
Explorar Skills
Desarrollo · 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

Documentación 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 guardadas