AgentSkillsCN

svelte5-sveltekit

精通Svelte 5与SvelteKit开发,涵盖基于Runes的响应式机制($state、$derived、$effect、$props)、基于文件的路由、表单操作、采用Zod验证的远程函数,以及各类最佳实践。适用于Svelte 5或SvelteKit项目,无论是创建组件、设置路由,还是实现表单与数据加载,均可使用此技能。

SKILL.md
--- frontmatter
name: svelte5-sveltekit
description: Svelte 5 and SvelteKit development expertise including runes-based reactivity ($state, $derived, $effect, $props), file-based routing, form actions, remote functions with Zod validation, and best practices. Use when working with Svelte 5 or SvelteKit projects, creating components, setting up routes, or implementing forms and data loading.

Svelte 5 & SvelteKit Development

Complete guide for modern Svelte 5 and SvelteKit development with runes, routing, and best practices.

Core Svelte 5 Concepts

Runes - The New Reactivity System

Svelte 5 introduces runes - compiler symbols prefixed with $ that provide explicit, universal reactivity. They work in .svelte, .svelte.js, and .svelte.ts files.

$state - Reactive State

svelte
<script>
  // Basic state
  let count = $state(0);
  
  // Object state (deeply reactive)
  let user = $state({
    name: 'Alice',
    age: 30
  });
  
  // Array state
  let items = $state([1, 2, 3]);
</script>

<button onclick={() => count++}>
  Count: {count}
</button>

Best Practices:

  • Use $state for all reactive variables
  • Objects and arrays are deeply reactive - mutations work
  • For raw state (non-reactive): use $state.raw()
  • No need to wrap class instances

$derived - Computed Values

svelte
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
  
  // For complex computations, use $derived.by
  let total = $derived.by(() => {
    let sum = 0;
    for (const n of numbers) {
      sum += n;
    }
    return sum;
  });
</script>

Best Practices:

  • Use $derived for computed values, NOT $effect
  • Keep derived expressions pure (no side effects)
  • No state changes inside $derived - will error
  • Automatically memoized - only recalculates when dependencies change

$effect - Side Effects

svelte
<script>
  let count = $state(0);
  
  $effect(() => {
    console.log(`Count is now ${count}`);
    
    // Cleanup function (optional)
    return () => {
      console.log('Cleanup before next run');
    };
  });
  
  // Runs before DOM updates
  $effect.pre(() => {
    // Code here runs before DOM updates
  });
</script>

Best Practices:

  • Use $effect for side effects ONLY (DOM manipulation, logging, external APIs)
  • Use $derived for computing values
  • Effects run after component mount and when dependencies change
  • Return cleanup functions for subscriptions/listeners

Common Anti-patterns to AVOID:

svelte
// ❌ BAD: Using $effect to compute values
$effect(() => {
  doubled = count * 2; // Should be $derived
});

// ✅ GOOD: Use $derived for computation
let doubled = $derived(count * 2);

// ❌ BAD: State changes in $derived
let bad = $derived(count++); // Will error

// ✅ GOOD: Pure computation
let good = $derived(count + 1);

$props - Component Props

svelte
<script>
  // Destructure props
  let { name, age = 18, optional } = $props();
  
  // With TypeScript
  interface Props {
    name: string;
    age?: number;
  }
  
  let { name, age = 18 }: Props = $props();
  
  // Bindable props
  let { value = $bindable(0) } = $props();
</script>

Best Practices:

  • Use $props() instead of export let (Svelte 4 syntax)
  • Provide default values inline
  • Use $bindable() for two-way binding
  • TypeScript interfaces improve type safety

Component Patterns

Event Handlers

svelte
<script>
  let count = $state(0);
  
  // Events are now just props
  let { onclick } = $props();
  
  function handleClick() {
    count++;
    onclick?.(); // Call parent handler if provided
  }
</script>

<button {onclick}>Click me</button>

<!-- Parent component -->
<MyButton onclick={() => console.log('clicked')} />

Best Practices:

  • Events are props now - no createEventDispatcher()
  • Use optional chaining for optional event handlers
  • Can check if handler exists: if (onclick) { ... }

Snippets - Reusable Markup

svelte
<script>
  let items = $state(['a', 'b', 'c']);
</script>

{#snippet card(title, content)}
  <div class="card">
    <h3>{title}</h3>
    <p>{content}</p>
  </div>
{/snippet}

{#each items as item}
  {@render card(item, `Content for ${item}`)}
{/each}

Lifecycle Hooks

svelte
<script>
  import { onMount, onDestroy } from 'svelte';
  
  // Still available and recommended for lifecycle
  onMount(() => {
    console.log('Component mounted');
    
    return () => {
      console.log('Cleanup on unmount');
    };
  });
  
  // Note: beforeUpdate and afterUpdate are deprecated
  // Use $effect instead when you need reactive cleanup
</script>

SvelteKit Architecture

File-based Routing

code
src/routes/
├── +page.svelte              # / route
├── +layout.svelte            # Root layout
├── +layout.server.ts         # Server layout load
├── about/
│   └── +page.svelte          # /about
├── blog/
│   ├── +page.svelte          # /blog
│   ├── [slug]/
│   │   └── +page.svelte      # /blog/[slug]
│   └── [...catchall]/
│       └── +page.svelte      # /blog/* (catch-all)
├── (authed)/                 # Route group (no URL segment)
│   ├── +layout.svelte        # Shared layout
│   ├── dashboard/
│   │   └── +page.svelte      # /dashboard
│   └── settings/
│       └── +page.svelte      # /settings
└── api/
    └── posts/
        └── +server.ts        # API endpoint

Key Files:

  • +page.svelte - Page component
  • +page.ts - Universal load function (runs on server and client)
  • +page.server.ts - Server-only load function
  • +layout.svelte - Layout wrapper
  • +layout.ts / +layout.server.ts - Layout data loading
  • +server.ts - API endpoints (GET, POST, etc.)
  • +error.svelte - Error boundary

Data Loading Patterns

Server Load Functions

typescript
// +page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
  // Runs only on server
  // Has access to: database, environment variables, cookies
  const post = await db.getPost(params.slug);
  
  return {
    post
  };
};

Universal Load Functions

typescript
// +page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ params, fetch, data }) => {
  // Runs on server AND client
  // Can access parent data via 'data'
  // Use for client-safe operations
  
  const response = await fetch(`/api/posts/${params.slug}`);
  const post = await response.json();
  
  return {
    post
  };
};

Best Practices:

  • Use +page.server.ts for database access, secrets, server-only logic
  • Use +page.ts for API calls, client-safe operations
  • Universal loads can access server load data via data param
  • Parent layout data accessible via await parent()
  • Use SvelteKit's enhanced fetch for automatic request tracking

Form Actions & Remote Functions

Traditional Form Actions

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

export const actions: Actions = {
  // Default action
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get('email');
    
    if (!email) {
      return fail(400, { email, missing: true });
    }
    
    // Process data...
    redirect(303, '/success');
  },
  
  // Named action
  login: async ({ request }) => {
    const data = await request.formData();
    // Handle login...
    return { success: true };
  }
};
svelte
<!-- +page.svelte -->
<script>
  import { enhance } from '$app/forms';
  let { data, form } = $props();
</script>

<!-- Default action -->
<form method="POST" use:enhance>
  <input name="email" />
  {#if form?.missing}
    <p class="error">Email is required</p>
  {/if}
  <button>Submit</button>
</form>

<!-- Named action -->
<form method="POST" action="?/login" use:enhance>
  <input name="username" />
  <button>Login</button>
</form>

Remote Functions (New Pattern)

typescript
// data.remote.ts
import { query, form } from '@sveltejs/kit';
import { z } from 'zod';

// Remote query
export const getPosts = query(async () => {
  return await db.posts.findMany();
});

// Remote form with Zod validation
const createPostSchema = z.object({
  title: z.string().min(1, 'Title is required'),
  content: z.string().min(10, 'Content must be at least 10 characters')
});

export const createPost = form(createPostSchema, async (data) => {
  const post = await db.posts.create(data);
  
  // Refresh queries
  getPosts.refresh();
  
  return { success: true, post };
});
svelte
<!-- +page.svelte -->
<script>
  import { createPost, getPosts } from './data.remote';
  
  let posts = getPosts();
</script>

<form {...createPost}>
  <input name="title" />
  <textarea name="content"></textarea>
  <button>Create</button>
</form>

{#each posts.data as post}
  <article>{post.title}</article>
{/each}

Form Action Best Practices:

  • Use use:enhance for progressive enhancement
  • Return validation errors with fail()
  • Use redirect() for navigation after success
  • Named actions for multiple forms: action="?/actionName"
  • Access form data in page via form prop
  • Use Zod for schema validation in remote functions

API Endpoints

typescript
// +server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ url, params }) => {
  const data = await fetchData(params.id);
  
  if (!data) {
    error(404, 'Not found');
  }
  
  return json(data);
};

export const POST: RequestHandler = async ({ request }) => {
  const data = await request.json();
  // Process...
  return json({ success: true }, { status: 201 });
};

Page Options

typescript
// +page.ts or +page.server.ts
export const prerender = true;  // Static generation
export const ssr = false;        // Disable SSR (SPA mode)
export const csr = true;         // Enable client-side rendering

Common Patterns

State Management

typescript
// stores.svelte.ts - Shared state with runes
export function createCounter(initial = 0) {
  let count = $state(initial);
  let doubled = $derived(count * 2);
  
  return {
    get count() { return count; },
    get doubled() { return doubled; },
    increment: () => count++,
    decrement: () => count--
  };
}

// Usage in components
import { createCounter } from './stores.svelte';
const counter = createCounter();

Handling Forms with Progressive Enhancement

svelte
<script>
  import { enhance } from '$app/forms';
  
  let { form } = $props();
  let loading = $state(false);
</script>

<form 
  method="POST" 
  use:enhance={() => {
    loading = true;
    
    return async ({ result, update }) => {
      loading = false;
      await update();
    };
  }}
>
  <input name="email" />
  {#if form?.errors}
    <p class="error">{form.errors.email}</p>
  {/if}
  <button disabled={loading}>
    {loading ? 'Submitting...' : 'Submit'}
  </button>
</form>

Error Handling

svelte
<!-- +error.svelte -->
<script>
  import { page } from '$app/state';
</script>

<h1>{page.status}</h1>
<p>{page.error.message}</p>

Navigation

svelte
<script>
  import { goto } from '$app/navigation';
  
  function navigate() {
    goto('/dashboard');
  }
</script>

<!-- Use native <a> for links -->
<a href="/about">About</a>

Migration from Svelte 4

What Changed

  • export let$props()
  • let (reactive) → $state()
  • $: statements → $derived() or $effect()
  • createEventDispatcher → Props/callbacks
  • Stores still work but runes preferred for new code

Quick Migration Example

svelte
<!-- Svelte 4 -->
<script>
  export let name;
  let doubled;
  $: doubled = count * 2;
  $: console.log(count);
</script>

<!-- Svelte 5 -->
<script>
  let { name } = $props();
  let doubled = $derived(count * 2);
  $effect(() => console.log(count));
</script>

Anti-patterns to Avoid

Don't use $effect for computed values

svelte
// Bad
$effect(() => {
  total = a + b;
});

// Good
let total = $derived(a + b);

Don't mutate state inside $derived

svelte
// Bad
let bad = $derived(count++);

// Good
let good = $derived(count + 1);

Don't use old Svelte 4 patterns in new code

svelte
// Bad
export let prop;
$: doubled = count * 2;

// Good
let { prop } = $props();
let doubled = $derived(count * 2);

Don't forget to use .server files for sensitive operations

typescript
// Bad: +page.ts (runs on client!)
export const load = async () => {
  const secret = process.env.SECRET_KEY; // Exposed to client!
};

// Good: +page.server.ts
export const load = async () => {
  const secret = process.env.SECRET_KEY; // Server-only
};

Performance Tips

  1. Use $derived over $effect for computations - More efficient
  2. Leverage fine-grained reactivity - Only affected parts re-render
  3. Use $state.raw() for non-reactive data - Avoid unnecessary tracking
  4. Prerender static pages - Set export const prerender = true
  5. Lazy load components - Use dynamic imports for code splitting
  6. Use SvelteKit's enhanced fetch - Automatic deduplication and caching

TypeScript Integration

typescript
// Component with typed props
<script lang="ts">
  interface Props {
    title: string;
    count?: number;
    onClick?: () => void;
  }
  
  let { title, count = 0, onClick }: Props = $props();
  
  // Derived with type inference
  let doubled: number = $derived(count * 2);
</script>

// Load function types
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params }) => {
  // Fully typed
};

Recommended Project Structure

code
src/
├── lib/
│   ├── components/
│   │   ├── ui/           # Reusable UI components
│   │   └── features/     # Feature-specific components
│   ├── stores.svelte.ts  # Shared reactive state
│   ├── utils/            # Utility functions
│   └── server/           # Server-only code
├── routes/
│   ├── +layout.svelte
│   ├── +page.svelte
│   └── ...
└── app.html

Key Takeaways

  1. Runes are universal - Work in components AND .svelte.js files
  2. $derived for values, $effect for side effects - Clear separation
  3. SvelteKit's routing is file-based - Conventions over configuration
  4. Progressive enhancement by default - Forms work without JS
  5. Type safety is built-in - Use TypeScript for best experience
  6. Server vs client is explicit - .server.ts files are server-only
  7. Use Zod for validation - Type-safe schema validation with excellent inference

Resources