返回 Skills 目录
ejirocodes/agent-skills包含需要注意的行为

SKILL DETAIL

svelte5-best-practices

ejirocodes/agent-skills/svelte5-best-practices

本技能提供 Svelte 5 的现代最佳实践,涵盖 runes($state、$derived、$effect、$props、$bindable、$inspect)、snippets({#snippet}、{@render})、事件处理、SvelteKit 数据加载、表单操作、从 Svelte 4 迁移、store 到 runes 迁移、slots 到 snippets 迁移、TypeScript 属性类型、泛型组件、SSR 状态隔离、性能优化和组件测试。 包含快速参考表、基本模式示例(如响应式状态、组件属性、snippets、事件处理、回调属性)以及常见错误列表,帮助开发者编写高效、可维护的 Svelte 5 应用。

安装量 · 235查看来源

Installation

npx skills add https://github.com/ejirocodes/agent-skills --skill svelte5-best-practices

技能文件

SKILL.md

最近同步 · 2026年9月28日

references/events.md›
# Svelte 5 Events Reference

## Table of Contents
- [Event Handler Syntax](#event-handler-syntax)
- [Callback Props Pattern](#callback-props-pattern)
- [Context API](#context-api)

---

## Event Handler Syntax

Svelte 5 replaces `on:click` directive syntax with standard HTML attribute syntax `onclick`.

### Basic Event Handlers

**Svelte 4:**
```svelte
<button on:click={handleClick}>Click</button>
<input on:input={handleInput} />
<form on:submit={handleSubmit}>...</form>
```

**Svelte 5:**
```svelte
<button onclick={handleClick}>Click</button>
<input oninput={handleInput} />
<form onsubmit={handleSubmit}>...</form>
```

### Event Modifiers Migration

Event modifiers no longer exist. Use wrapper functions:

**Svelte 4:**
```svelte
<form on:submit|preventDefault={handleSubmit}>...</form>
<button on:click|stopPropagation={handleClick}>...</button>
```

**Svelte 5:**
```svelte
<script>
  function handleSubmit(event) {
    event.preventDefault();
    // ... handle form
  }

  function handleClick(event) {
    event.stopPropagation();
    // ... handle click
  }
</script>

<form onsubmit={handleSubmit}>...</form>
<button onclick={handleClick}>...</button>
```

### Capture, Passive, and NonPassive

```svelte
<!-- Capture phase -->
<div onclickcapture={handleCapture}>
  <button onclick={handleClick}>Click</button>
</div>

<!-- Passive listener -->
<div ontouchstartpassive={handleTouch}>...</div>

<!-- Non-passive -->
<div ontouchmovenonpassive={(e) => e.preventDefault()}>...</div>
```

### Inline Handlers

```svelte
<button onclick={() => count++}>Count: {count}</button>
<input oninput={(e) => name = e.target.value} />
```

### Event Handler Shorthand

```svelte
<script>
  function onclick(event) {
    console.log('Clicked!', event);
  }
</script>

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

### Spreading Event Handlers

```svelte
<script>
  let handlers = {
    onclick: () => console.log('clicked'),
    onmouseenter: () => console.log('entered'),
    onmouseleave: () => console.log('left')
  };
</script>

<button {...handlers}>Hover or Click</button>
```

### Multiple Handlers for Same Event

In Svelte 5, combine logic into one handler:

```svelte
<script>
  function handleClick(event) {
    handler1(event);
    handler2(event);
  }
</script>

<button onclick={handleClick}>...</button>
```

### TypeScript Event Typing

```svelte
<script lang="ts">
  function handleClick(event: MouseEvent) {
    console.log(event.clientX, event.clientY);
  }

  function handleInput(event: Event) {
    const target = event.target as HTMLInputElement;
    console.log(target.value);
  }

  function handleSubmit(event: SubmitEvent) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget as HTMLFormElement);
  }
</script>
```

---

## Callback Props Pattern

Svelte 5 deprecates `createEventDispatcher` in favor of callback props for component-to-parent communication.

### Basic Event Pattern

**Svelte 4:**
```svelte
<!-- Button.svelte -->
<script>
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();
</script>

<button on:click={() => dispatch('click', { timestamp: Date.now() })}>Click</button>

<!-- Parent.svelte -->
<Button on:click={(e) => console.log(e.detail)} />
```

**Svelte 5:**
```svelte
<!-- Button.svelte -->
<script>
  let { onclick } = $props();
</script>

<button onclick={() => onclick?.({ timestamp: Date.now() })}>Click</button>

<!-- Parent.svelte -->
<Button onclick={(data) => console.log(data)} />
```

### Multiple Callbacks

```svelte
<!-- Dialog.svelte -->
<script>
  let { onconfirm, oncancel, onclose } = $props();
</script>

<dialog>
  <button onclick={() => onconfirm?.()}>Confirm</button>
  <button onclick={() => oncancel?.()}>Cancel</button>
  <button onclick={() => onclose?.()}>X</button>
</dialog>

<!-- Parent.svelte -->
<Dialog
  onconfirm={() => save()}
  oncancel={() => reset()}
  onclose={() => visible = false}
/>
```

### Typed Callbacks with TypeScript

```svelte
<script lang="ts">
  interface Props {
    value?: string;
    onsearch?: (query: string) => void;
    onchange?: (value: string) => void;
    onclear?: () => void;
  }

  let { value = '', onsearch, onchange, onclear }: Props = $props();

  function handleInput(e: Event) {
    const newValue = (e.target as HTMLInputElement).value;
    value = newValue;
    onchange?.(newValue);
  }
</script>
```

### Forwarding Native Events

```svelte
<script lang="ts">
  import type { MouseEventHandler } from 'svelte/elements';

  interface Props {
    onclick?: MouseEventHandler<HTMLButtonElement>;
    children: import('svelte').Snippet;
  }

  let { onclick, children, ...rest }: Props = $props();
</script>

<button {onclick} {...rest}>{@render children()}</button>
```

---

## Context API

Context functions must be called synchronously during component initialization.

### Correct Context Usage

```svelte
<script>
  import { setContext, getContext } from 'svelte';

  // These run during initialization - CORRECT
  setContext('theme', 'dark');
  const theme = getContext('theme');

  // INCORRECT - in $effect or callback
  // $effect(() => { setContext('theme', 'dark'); }); // ERROR
</script>
```

### Reactive Context Values

Pass `$state` objects for reactive context:

```svelte
<!-- Provider.svelte -->
<script>
  import { setContext } from 'svelte';

  let theme = $state('light');

  setContext('theme', {
    get current() { return theme; },
    toggle() { theme = theme === 'light' ? 'dark' : 'light'; }
  });

  let { children } = $props();
</script>

{@render children()}

<!-- Consumer.svelte -->
<script>
  import { getContext } from 'svelte';
  const themeContext = getContext('theme');
</script>

<p>Theme: {themeContext.current}</p>
<button onclick={themeContext.toggle}>Toggle</button>
```

### Type-Safe Context Pattern

```ts
// context.ts
import { setContext, getContext } from 'svelte';

const THEME_KEY = Symbol('theme');

interface ThemeContext {
  current: 'light' | 'dark';
  toggle: () => void;
}

export function setThemeContext(context: ThemeContext) {
  setContext(THEME_KEY, context);
}

export function getThemeContext(): ThemeContext {
  const context = getContext<ThemeContext>(THEME_KEY);
  if (!context) throw new Error('Theme context not found');
  return context;
}
```

### Avoid Generic Keys

```svelte
<script>
  // Option 1: Symbol
  const USER_KEY = Symbol('user');
  setContext(USER_KEY, userData);

  // Option 2: Unique string
  setContext('myapp:user', userData);

  // Option 3: Object key
  const userKey = {};
  setContext(userKey, userData);
</script>
```

### hasContext Check

```svelte
<script>
  import { hasContext, getContext } from 'svelte';

  const hasTheme = hasContext('theme');
  const theme = hasTheme ? getContext('theme') : { current: 'light' };
</script>
```

### Context Boundaries

Context flows down the component tree:

```svelte
<!-- Parent.svelte -->
<script>
  import { getContext, setContext } from 'svelte';
  const level = getContext('level'); // 0
  setContext('level', level + 1); // Override for children
</script>

<Child /> <!-- Will see level = 1 -->
```
references/migration.md›
# Svelte 4 to Svelte 5 Migration Reference

## Table of Contents
- [Migrating Reactive Statements](#migrating-reactive-statements)
- [Migrating Stores to Runes](#migrating-stores-to-runes)

---

## Migrating Reactive Statements

Svelte 5 replaces `$:` with `$derived` (for values) and `$effect` (for side effects).

### Computed Values: $: to $derived

**Svelte 4:**
```svelte
<script>
  let count = 0;
  $: doubled = count * 2;
  $: quadrupled = doubled * 2;
</script>
```

**Svelte 5:**
```svelte
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
  let quadrupled = $derived(doubled * 2);
</script>
```

### Complex Computed Values: $derived.by

**Svelte 4:**
```svelte
<script>
  let items = [];
  let filter = 'all';

  $: filteredItems = {
    if (filter === 'all') return items;
    if (filter === 'active') return items.filter(i => !i.done);
    return items.filter(i => i.done);
  };
</script>
```

**Svelte 5:**
```svelte
<script>
  let items = $state([]);
  let filter = $state('all');

  let filteredItems = $derived.by(() => {
    if (filter === 'all') return items;
    if (filter === 'active') return items.filter(i => !i.done);
    return items.filter(i => i.done);
  });
</script>
```

### Side Effects: $: to $effect

**Svelte 4:**
```svelte
<script>
  let count = 0;
  $: console.log('Count changed:', count);
  $: document.title = `Count: ${count}`;
</script>
```

**Svelte 5:**
```svelte
<script>
  let count = $state(0);

  $effect(() => { console.log('Count changed:', count); });
  $effect(() => { document.title = `Count: ${count}`; });
</script>
```

### Conditional Side Effects

**Svelte 4:**
```svelte
<script>
  let value;
  $: if (value > 100) { alert('Value too high!'); }
</script>
```

**Svelte 5:**
```svelte
<script>
  let value = $state(0);

  $effect(() => {
    if (value > 100) { alert('Value too high!'); }
  });
</script>
```

### Props Migration

**Svelte 4:**
```svelte
<script>
  export let name;
  export let count = 0;
  $: greeting = `Hello, ${name}!`;
</script>
```

**Svelte 5:**
```svelte
<script>
  let { name, count = 0 } = $props();
  let greeting = $derived(`Hello, ${name}!`);
</script>
```

### Effect with Cleanup

**Svelte 4:**
```svelte
<script>
  import { onDestroy } from 'svelte';

  let count = 0;
  let interval;

  $: {
    clearInterval(interval);
    interval = setInterval(() => console.log(count), 1000);
  }

  onDestroy(() => clearInterval(interval));
</script>
```

**Svelte 5:**
```svelte
<script>
  let count = $state(0);

  $effect(() => {
    const interval = setInterval(() => console.log(count), 1000);
    return () => clearInterval(interval); // Cleanup built-in
  });
</script>
```

### Migration Cheat Sheet

| Svelte 4 Pattern | Svelte 5 Replacement |
|------------------|----------------------|
| `let x = 0` (in component) | `let x = $state(0)` |
| `export let prop` | `let { prop } = $props()` |
| `$: derived = expr` | `let derived = $derived(expr)` |
| `$: { complex }` (value) | `let x = $derived.by(() => { ... })` |
| `$: console.log(x)` | `$effect(() => console.log(x))` |
| `$: if (x) { ... }` | `$effect(() => { if (x) { ... } })` |
| `$: document.title = x` | `$effect(() => { document.title = x })` |

### Automated Migration

```bash
npx sv migrate svelte-5
```

---

## Migrating Stores to Runes

Svelte 5 runes can replace most store use cases with simpler, more direct reactive state.

### Local Component State

**Svelte 4:**
```svelte
<script>
  import { writable } from 'svelte/store';

  const count = writable(0);
  function increment() { count.update(n => n + 1); }
</script>

<button on:click={increment}>Count: {$count}</button>
```

**Svelte 5:**
```svelte
<script>
  let count = $state(0);
  function increment() { count++; }
</script>

<button onclick={increment}>Count: {count}</button>
```

### Shared State Across Components

**Svelte 4 (stores.ts):**
```ts
import { writable } from 'svelte/store';
export const user = writable(null);
export const theme = writable('light');
```

**Svelte 5 (state.svelte.ts):**
```ts
export const user = $state<User | null>(null);
export const theme = $state({ current: 'light' as 'light' | 'dark' });

export function setTheme(newTheme: 'light' | 'dark') {
  theme.current = newTheme;
}
```

```svelte
<script>
  import { theme, setTheme } from './state.svelte';
</script>

<p>Theme: {theme.current}</p>
<button onclick={() => setTheme('dark')}>Dark Mode</button>
```

### Derived Store to $derived

**Svelte 4:**
```ts
import { writable, derived } from 'svelte/store';
export const items = writable([]);
export const completedCount = derived(items, $items =>
  $items.filter(i => i.done).length
);
```

**Svelte 5:**
```ts
// state.svelte.ts
export const items = $state<Item[]>([]);

export function getCompletedCount() {
  return items.filter(i => i.done).length;
}
```

```svelte
<script>
  import { items } from './state.svelte';
  let completedCount = $derived(items.filter(i => i.done).length);
</script>
```

### Custom Store to Reactive Class

**Svelte 4:**
```ts
function createCounter() {
  const { subscribe, set, update } = writable(0);
  return {
    subscribe,
    increment: () => update(n => n + 1),
    decrement: () => update(n => n - 1),
    reset: () => set(0)
  };
}
export const counter = createCounter();
```

**Svelte 5:**
```ts
// counter.svelte.ts
class Counter {
  value = $state(0);
  increment() { this.value++; }
  decrement() { this.value--; }
  reset() { this.value = 0; }
}

export const counter = new Counter();
```

```svelte
<script>
  import { counter } from './counter.svelte';
</script>

<p>{counter.value}</p>
<button onclick={() => counter.increment()}>+</button>
```

### Async State Pattern

**Svelte 5:**
```ts
// api.svelte.ts
export const state = $state({
  data: null as Data | null,
  loading: false,
  error: null as Error | null
});

export async function fetchData() {
  state.loading = true;
  state.error = null;

  try {
    const res = await fetch('/api/data');
    state.data = await res.json();
  } catch (e) {
    state.error = e as Error;
  } finally {
    state.loading = false;
  }
}
```

### When to Still Use Stores

1. **Library interop** - Libraries that expect Svelte stores
2. **Legacy code** - Gradual migration
3. **Observable patterns** - When you need the subscribe API
4. **Server-side rendering** - Stores have built-in SSR handling

### Store to Rune Cheat Sheet

| Store Pattern | Rune Replacement |
|---------------|------------------|
| `writable(value)` | `$state(value)` |
| `$store` (auto-subscribe) | Direct access to `$state` value |
| `store.set(x)` | `state = x` |
| `store.update(fn)` | Direct mutation or reassignment |
| `derived(store, fn)` | `$derived(fn())` in component |
| `readable(value, start)` | `$state` + `$effect` for setup |
references/performance.md›
# Svelte 5 Performance Reference

## Table of Contents
- [Universal Reactivity](#universal-reactivity)
- [Avoiding Over-Reactivity](#avoiding-over-reactivity)
- [Preventing Load Waterfalls](#preventing-load-waterfalls)
- [Streaming Non-Critical Data](#streaming-non-critical-data)
- [Component Testing](#component-testing)

---

## Universal Reactivity

Svelte 5 allows runes in `.svelte.js` or `.svelte.ts` files for shared state outside components.

### Shared Counter State

```ts
// counter.svelte.ts
export const counter = $state({ count: 0 });

export function increment() { counter.count++; }
export function decrement() { counter.count--; }
export function reset() { counter.count = 0; }
```

```svelte
<script>
  import { counter, increment } from './counter.svelte';
</script>

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

### Object State with Getters

```ts
// user.svelte.ts
const state = $state<User>({ firstName: '', lastName: '', email: '' });

export const user = {
  get firstName() { return state.firstName; },
  set firstName(v: string) { state.firstName = v; },
  get fullName() { return `${state.firstName} ${state.lastName}`; },
  get isValid() { return state.firstName && state.email.includes('@'); }
};
```

### Reactive Class Pattern

```ts
// todo.svelte.ts
class TodoStore {
  items = $state<Todo[]>([]);
  filter = $state<'all' | 'active' | 'completed'>('all');

  get filtered() {
    switch (this.filter) {
      case 'active': return this.items.filter(t => !t.done);
      case 'completed': return this.items.filter(t => t.done);
      default: return this.items;
    }
  }

  add(text: string) {
    this.items.push({ id: Date.now(), text, done: false });
  }
}

export const todos = new TodoStore();
```

### Important Notes

1. **File extension matters**: Must use `.svelte.js` or `.svelte.ts`
2. **No $derived in module scope**: Use getters for derived values
3. **SSR caution**: Avoid initializing browser-only state at module level

---

## Avoiding Over-Reactivity

Common mistakes with runes can cause unnecessary re-renders, infinite loops, or degraded performance.

### Anti-Pattern 1: Using $effect to Set Derived Values

**WRONG:**
```svelte
<script>
  let count = $state(0);
  let doubled = $state(0);
  $effect(() => { doubled = count * 2; }); // Anti-pattern!
</script>
```

**CORRECT:**
```svelte
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>
```

### Anti-Pattern 2: Circular Dependencies

**WRONG:**
```svelte
<script>
  let a = $state(1);
  let b = $state(2);

  $effect(() => {
    if (a > 10) b = 0;
    if (b > 10) a = 0; // Triggers the effect again!
  });
</script>
```

**CORRECT: Use separate effects or event handlers**

### Anti-Pattern 3: Not Using untrack

**WRONG:**
```svelte
<script>
  let count = $state(0);
  let log = $state([]);

  $effect(() => {
    log.push(`Count is ${count}`); // log change triggers re-run!
  });
</script>
```

**CORRECT:**
```svelte
<script>
  import { untrack } from 'svelte';

  let count = $state(0);
  let log = $state([]);

  $effect(() => {
    untrack(() => { log.push(`Count is ${count}`); });
  });
</script>
```

### Anti-Pattern 4: Heavy Computations in $derived

**WRONG:**
```svelte
<script>
  let items = $state([/* thousands */]);
  let filter = $state('');

  let filtered = $derived(
    items.filter(item => JSON.stringify(item).includes(filter))
  );
</script>
```

**CORRECT: Debounce expensive computations**
```svelte
<script>
  let filter = $state('');
  let debouncedFilter = $state('');

  $effect(() => {
    const timeout = setTimeout(() => { debouncedFilter = filter; }, 300);
    return () => clearTimeout(timeout);
  });

  let filtered = $derived(
    items.filter(item => item.name.includes(debouncedFilter))
  );
</script>
```

### Anti-Pattern 5: Effect for DOM Manipulation

**WRONG:**
```svelte
<script>
  let visible = $state(false);
  let element;

  $effect(() => {
    if (element) element.style.display = visible ? 'block' : 'none';
  });
</script>
```

**CORRECT:**
```svelte
{#if visible}<div>Content</div>{/if}
<!-- Or -->
<div class:hidden={!visible}>Content</div>
```

### Performance Tips

1. Use `$derived` over `$effect` for computed values
2. Debounce expensive reactive computations
3. Use `untrack` to prevent unnecessary dependencies
4. Group related state into objects
5. Let Svelte handle DOM updates

---

## Preventing Load Waterfalls

Sequential API calls multiply latency. Use parallel requests and streaming.

### Anti-Pattern: Waterfall

**WRONG (3 seconds total):**
```ts
export const load = async ({ fetch }) => {
  const user = await fetch('/api/user').then(r => r.json());     // 1s
  const posts = await fetch(`/api/users/${user.id}/posts`).then(r => r.json()); // 1s
  const comments = await fetch('/api/comments').then(r => r.json()); // 1s
  return { user, posts, comments };
};
```

### Pattern 1: Parallel with Promise.all

**CORRECT (1 second total):**
```ts
export const load = async ({ fetch }) => {
  const [user, posts, comments] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json())
  ]);
  return { user, posts, comments };
};
```

### Pattern 2: Partial Dependencies

```ts
export const load = async ({ fetch }) => {
  const user = await fetch('/api/user').then(r => r.json());

  const [posts, followers] = await Promise.all([
    fetch(`/api/users/${user.id}/posts`).then(r => r.json()),
    fetch(`/api/users/${user.id}/followers`).then(r => r.json())
  ]);

  return { user, posts, followers };
};
```

### Pattern 3: Streaming Non-Critical Data

```ts
export const load = async ({ fetch }) => {
  const user = await fetch('/api/user').then(r => r.json());

  return {
    user,
    recommendations: fetch('/api/recommendations').then(r => r.json()),
    analytics: fetch('/api/analytics').then(r => r.json())
  };
};
```

### Performance Comparison

| Pattern | 3 APIs x 1s each |
|---------|------------------|
| Sequential | 3 seconds |
| Parallel (Promise.all) | 1 second |
| Streaming | 0s initial, streams rest |

---

## Streaming Non-Critical Data

Return promises from load functions to stream non-essential data after initial page render.

### Basic Streaming

**WRONG (blocks on slow data):**
```ts
export const load = async ({ fetch }) => {
  const user = await fetch('/api/user').then(r => r.json());     // 100ms
  const analytics = await fetch('/api/analytics').then(r => r.json()); // 2000ms
  return { user, analytics }; // Page blocked for 2.1 seconds
};
```

**CORRECT (stream slow data):**
```ts
export const load = async ({ fetch }) => {
  const user = await fetch('/api/user').then(r => r.json());
  const analytics = fetch('/api/analytics').then(r => r.json()); // Don't await

  return {
    user,      // Available in 100ms
    analytics  // Streams when ready
  };
};
```

### Component Handling

```svelte
<script lang="ts">
  import type { PageProps } from './$types';
  let { data }: PageProps = $props();
</script>

<header><h1>Welcome, {data.user.name}</h1></header>

<aside>
  {#await data.analytics}
    <div class="skeleton">Loading analytics...</div>
  {:then analytics}
    <div class="analytics">
      <p>Page views: {analytics.views}</p>
    </div>
  {:catch}
    <div class="error">Failed to load analytics</div>
  {/await}
</aside>
```

### When to Stream

| Data Type | Stream? | Reason |
|-----------|---------|--------|
| User info | No | Critical for layout |
| Main content | No | Users came for this |
| Analytics | Yes | Not user-facing |
| Recommendations | Yes | Supplementary |
| Comments | Maybe | Important but can load later |

---

## Component Testing

Svelte 5 components with runes require proper Vitest configuration.

### Option 1: Vitest Browser Mode (Recommended)

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

export default defineConfig({
  plugins: [svelte()],
  test: {
    browser: {
      enabled: true,
      provider: 'playwright',
      name: 'chromium'
    }
  }
});
```

### Option 2: JSDOM with @testing-library/svelte

```ts
// 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: {
    environment: 'jsdom',
    include: ['src/**/*.{test,spec}.{js,ts}'],
    globals: true
  }
});
```

### Basic Component Test

```ts
import { render, screen, fireEvent } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';

test('increments count when clicked', async () => {
  render(Counter);

  const button = screen.getByRole('button');
  expect(button).toHaveTextContent('Count: 0');

  await fireEvent.click(button);
  expect(button).toHaveTextContent('Count: 1');
});
```

### Testing Props

```ts
test('renders with custom name', () => {
  render(Greeting, { props: { name: 'Alice' } });
  expect(screen.getByText('Hello, Alice!')).toBeInTheDocument();
});
```

### Testing Callbacks

```ts
import { vi } from 'vitest';

test('calls onclick callback when clicked', async () => {
  const handleClick = vi.fn();
  render(Button, { props: { onclick: handleClick } });

  await fireEvent.click(screen.getByRole('button'));
  expect(handleClick).toHaveBeenCalledTimes(1);
});
```

### Testing Async Components

```ts
import { waitFor } from '@testing-library/svelte';

test('loads and displays user data', async () => {
  global.fetch = vi.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ name: 'Alice' })
  });

  render(UserProfile, { props: { userId: '123' } });
  expect(screen.getByText('Loading...')).toBeInTheDocument();

  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
  });
});
```
references/runes.md›
# Svelte 5 Runes Reference

## Table of Contents
- [$state - Reactive State](#state---reactive-state)
- [$derived - Computed Values](#derived---computed-values)
- [$effect - Side Effects](#effect---side-effects)
- [$props - Component Props](#props---component-props)
- [$bindable - Two-Way Binding](#bindable---two-way-binding)
- [$inspect - Debugging](#inspect---debugging)

---

## $state - Reactive State

In Svelte 5, plain `let` declarations are no longer automatically reactive. Use `$state()` for reactive state.

### Basic Usage

```svelte
<script>
  let count = $state(0);
</script>

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

### Object and Array State

Objects and arrays are deeply reactive by default:

```svelte
<script>
  let user = $state({ name: 'Alice', age: 30 });
  let items = $state(['apple', 'banana']);
</script>

<button onclick={() => user.age++}>Age: {user.age}</button>
<button onclick={() => items.push('cherry')}>Items: {items.length}</button>
```

### Class State

```svelte
<script>
  class Counter {
    count = $state(0);
    increment() { this.count++; }
  }
  const counter = new Counter();
</script>

<button onclick={() => counter.increment()}>{counter.count}</button>
```

### Raw State (Opt-out of Deep Reactivity)

```svelte
<script>
  let items = $state.raw([1, 2, 3]);

  // This WON'T trigger an update:
  items.push(4);

  // This WILL trigger an update:
  items = [...items, 4];
</script>
```

---

## $derived - Computed Values

Replaces `$:` reactive statements for computed values.

### Basic Usage

```svelte
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
  let quadrupled = $derived(doubled * 2);
</script>
```

### Complex Derivations with $derived.by

```svelte
<script>
  let items = $state([1, 2, 3, 4, 5]);
  let filter = $state('even');

  let filteredItems = $derived.by(() => {
    if (filter === 'even') return items.filter(n => n % 2 === 0);
    return items.filter(n => n % 2 !== 0);
  });
</script>
```

### Deriving from Props

```svelte
<script>
  let { firstName, lastName } = $props();
  let fullName = $derived(`${firstName} ${lastName}`);
</script>
```

### $derived vs $effect

Use `$derived` for computing values, `$effect` for side effects:

```svelte
<script>
  let count = $state(0);

  // CORRECT: Use $derived for computed values
  let doubled = $derived(count * 2);

  // INCORRECT: Don't use $effect to set derived values
  // let doubled;
  // $effect(() => { doubled = count * 2; }); // Anti-pattern!
</script>
```

---

## $effect - Side Effects

Runs code when component mounts and when dependencies change. Requires cleanup for subscriptions.

### Basic Effect with Cleanup

```svelte
<script>
  let count = $state(0);

  $effect(() => {
    const interval = setInterval(() => count++, 1000);
    return () => clearInterval(interval); // Cleanup
  });
</script>
```

### DOM Event Listeners

```svelte
<script>
  let mouseX = $state(0);
  let mouseY = $state(0);

  $effect(() => {
    function handleMouseMove(event) {
      mouseX = event.clientX;
      mouseY = event.clientY;
    }
    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  });
</script>
```

### External Subscriptions

```svelte
<script>
  let { userId } = $props();
  let userData = $state(null);

  $effect(() => {
    const unsubscribe = database.subscribe(`users/${userId}`, (data) => {
      userData = data;
    });
    return () => unsubscribe();
  });
</script>
```

### $effect.pre for Pre-DOM Updates

```svelte
<script>
  let div;
  let messages = $state([]);

  $effect.pre(() => {
    if (div) {
      const shouldScroll = div.scrollTop + div.clientHeight >= div.scrollHeight - 20;
      if (shouldScroll) {
        tick().then(() => { div.scrollTop = div.scrollHeight; });
      }
    }
  });
</script>
```

### Untracked Dependencies

```svelte
<script>
  import { untrack } from 'svelte';

  let count = $state(0);
  let logCount = $state(0);

  $effect(() => {
    // Only runs when count changes, not logCount
    console.log(count, untrack(() => logCount));
  });
</script>
```

---

## $props - Component Props

Replaces `export let` for declaring component props.

### Basic Usage

```svelte
<script>
  let { name, count = 0, disabled = false } = $props();
</script>
```

### Rest Props Pattern

```svelte
<script>
  let { class: className, children, ...restProps } = $props();
</script>

<div class={className} {...restProps}>
  {@render children?.()}
</div>
```

### Renaming Reserved Words

```svelte
<script>
  let {
    class: className,  // 'class' is reserved
    for: htmlFor,      // 'for' is reserved
    ...rest
  } = $props();
</script>
```

### TypeScript Props

```svelte
<script lang="ts">
  interface Props {
    name: string;
    count?: number;
    onClick?: (event: MouseEvent) => void;
  }

  let { name, count = 0, onClick }: Props = $props();
</script>
```

### Props with Children

```svelte
<script lang="ts">
  import type { Snippet } from 'svelte';

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

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

<article>
  <h1>{title}</h1>
  <main>{@render children()}</main>
  {#if footer}<footer>{@render footer()}</footer>{/if}
</article>
```

---

## $bindable - Two-Way Binding

Props must be explicitly marked with `$bindable()` to support `bind:`.

### Basic Usage

```svelte
<!-- Input.svelte -->
<script>
  let { value = $bindable() } = $props();
</script>

<input bind:value />

<!-- Parent.svelte -->
<script>
  import Input from './Input.svelte';
  let name = $state('');
</script>

<Input bind:value={name} />
```

### Bindable with Default Value

```svelte
<script>
  let { value = $bindable('default') } = $props();
</script>
```

### Multiple Bindable Props

```svelte
<script>
  let {
    value = $bindable(''),
    checked = $bindable(false),
    selected = $bindable(null)
  } = $props();
</script>
```

### TypeScript with Bindable

```svelte
<script lang="ts">
  interface Props {
    value: string;
    disabled?: boolean;
  }

  let { value = $bindable(''), disabled = false }: Props = $props();
</script>
```

---

## $inspect - Debugging

Development-only debugging that logs values when they change. Stripped in production.

### Basic Usage

```svelte
<script>
  let count = $state(0);
  $inspect(count); // Logs every time count changes
</script>
```

### Multiple Values

```svelte
<script>
  let name = $state('Alice');
  let age = $state(30);
  $inspect(name, age);
</script>
```

### Custom Logging with $inspect.with

```svelte
<script>
  let user = $state({ name: 'Alice', age: 30 });

  $inspect.with((type, ...values) => {
    if (type === 'init') console.log('Initial value:', values);
    else console.log('Updated to:', values);
  }, user);
</script>
```

### Debugging with Breakpoints

```svelte
<script>
  let data = $state({ x: 0, y: 0 });

  $inspect.with((type, value) => {
    if (type === 'update' && value.x > 100) debugger;
  }, data);
</script>
```
references/snippets.md›
# Svelte 5 Snippets Reference

## Table of Contents
- [Replacing Slots with Snippets](#replacing-slots-with-snippets)
- [Rendering with @render](#rendering-with-render)

---

## Replacing Slots with Snippets

Svelte 5 replaces `<slot>` with snippets - a more powerful and type-safe composition primitive.

### Default Content (children)

**Svelte 4:**
```svelte
<div class="card"><slot /></div>
```

**Svelte 5:**
```svelte
<script>
  let { children } = $props();
</script>

<div class="card">{@render children?.()}</div>
```

### Named Slots to Named Snippets

**Svelte 4:**
```svelte
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>

<!-- Usage -->
<Layout>
  <h1 slot="header">Title</h1>
  <p>Content</p>
  <span slot="footer">Footer</span>
</Layout>
```

**Svelte 5:**
```svelte
<script>
  let { header, children, footer } = $props();
</script>

<header>{@render header?.()}</header>
<main>{@render children?.()}</main>
<footer>{@render footer?.()}</footer>

<!-- Usage -->
<Layout>
  {#snippet header()}<h1>Title</h1>{/snippet}
  <p>Content</p>
  {#snippet footer()}<span>Footer</span>{/snippet}
</Layout>
```

### Slot Props to Snippet Parameters

**Svelte 4:**
```svelte
<ul>
  {#each items as item}
    <li><slot {item} /></li>
  {/each}
</ul>

<List {items} let:item>
  <span>{item.name}</span>
</List>
```

**Svelte 5:**
```svelte
<script>
  let { items, children } = $props();
</script>

<ul>
  {#each items as item}
    <li>{@render children?.(item)}</li>
  {/each}
</ul>

<List {items}>
  {#snippet children(item)}
    <span>{item.name}</span>
  {/snippet}
</List>
```

### Slot Fallback to Snippet Fallback

```svelte
<script>
  let { children } = $props();
</script>

{#if children}
  {@render children()}
{:else}
  <p>Default content</p>
{/if}
```

### Defining Snippets Within Components

```svelte
<script>
  let items = $state([
    { id: 1, name: 'Item 1', type: 'normal' },
    { id: 2, name: 'Item 2', type: 'featured' }
  ]);
</script>

{#snippet normalItem(item)}
  <li>{item.name}</li>
{/snippet}

{#snippet featuredItem(item)}
  <li class="featured">*** {item.name} ***</li>
{/snippet}

<ul>
  {#each items as item}
    {#if item.type === 'featured'}
      {@render featuredItem(item)}
    {:else}
      {@render normalItem(item)}
    {/if}
  {/each}
</ul>
```

### TypeScript Typing

```svelte
<script lang="ts">
  import type { Snippet } from 'svelte';

  interface Item { id: number; name: string; }

  interface Props {
    header?: Snippet;
    children: Snippet<[item: Item]>;
    footer?: Snippet;
  }

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

---

## Rendering with @render

Snippets must be rendered using `{@render}`. Optional snippets need null-safe calling.

### Basic Snippet Rendering

```svelte
<script>
  let { children } = $props();
</script>

<div>{@render children()}</div>
```

### Optional Snippets

Always use optional chaining for snippets that might not be provided:

```svelte
<script>
  let { children } = $props();
</script>

{@render children?.()}
```

### Conditional Rendering

```svelte
<script>
  let { header, children, footer } = $props();
</script>

{#if header}
  <header>{@render header()}</header>
{/if}

<main>{@render children?.()}</main>

{#if footer}
  <footer>{@render footer()}</footer>
{/if}
```

### Rendering with Arguments

```svelte
<script>
  let { items, itemTemplate } = $props();
</script>

<ul>
  {#each items as item, index}
    <li>{@render itemTemplate(item, index)}</li>
  {/each}
</ul>

<!-- Usage -->
<List {items}>
  {#snippet itemTemplate(item, index)}
    <span>{index + 1}. {item.name}</span>
  {/snippet}
</List>
```

### Rendering Multiple Times

Snippets can be rendered multiple times:

```svelte
<script>
  let { icon } = $props();
</script>

<button>
  {@render icon?.()}
  <span>Click me</span>
  {@render icon?.()}
</button>
```

### Rendering Dynamic Snippets

```svelte
<script>
  let { normalView, editView, isEditing } = $props();
  let currentView = $derived(isEditing ? editView : normalView);
</script>

{@render currentView?.()}
```

### TypeScript with @render

```svelte
<script lang="ts">
  import type { Snippet } from 'svelte';

  interface Props {
    children: Snippet;
    header?: Snippet;
    row: Snippet<[data: { id: number; name: string }]>;
  }

  let { children, header, row }: Props = $props();
</script>

{@render header?.()}
{@render children()}
{@render row({ id: 1, name: 'Test' })}
```

### Passing Snippets to Child Components

```svelte
<!-- Wrapper.svelte -->
<script>
  import Inner from './Inner.svelte';
  let { itemRenderer } = $props();
</script>

<Inner {itemRenderer} />

<!-- Inner.svelte -->
<script>
  let { itemRenderer } = $props();
</script>

{#each items as item}
  {@render itemRenderer?.(item)}
{/each}
```
references/sveltekit.md›
# SvelteKit Patterns Reference

## Table of Contents
- [Load Function Types](#load-function-types)
- [Page Props Typing](#page-props-typing)
- [Form Actions Error Handling](#form-actions-error-handling)
- [SSR State Isolation](#ssr-state-isolation)

---

## Load Function Types

SvelteKit has two load function types: universal (`+page.js`) and server-only (`+page.server.ts`).

### When to Use Each

| Use +page.server.ts | Use +page.js |
|---------------------|--------------|
| Accessing secrets/credentials | Public APIs only |
| Database connections | Non-serializable data (functions, classes) |
| Server-only APIs | Client-side caching benefits |
| Sensitive business logic | |

### Security: Secrets in Server Load

**WRONG (secrets exposed):**
```ts
// +page.js - DANGEROUS: runs in browser!
export const load = async ({ fetch }) => {
  const response = await fetch('https://api.stripe.com/charges', {
    headers: { 'Authorization': `Bearer ${STRIPE_SECRET_KEY}` } // EXPOSED!
  });
  return { charges: await response.json() };
};
```

**CORRECT:**
```ts
// +page.server.ts - only runs on server
import { STRIPE_SECRET_KEY } from '$env/static/private';

export const load = async ({ fetch }) => {
  const response = await fetch('https://api.stripe.com/charges', {
    headers: { 'Authorization': `Bearer ${STRIPE_SECRET_KEY}` }
  });
  return { charges: await response.json() };
};
```

### Serialization: Non-serializable in Universal Load

**WRONG (server load can't serialize functions):**
```ts
// +page.server.ts
export const load = async () => {
  return {
    formatDate: (date: Date) => date.toLocaleDateString(), // ERROR
    parser: new DOMParser() // ERROR
  };
};
```

**CORRECT:**
```ts
// +page.js
export const load = async () => {
  return {
    formatDate: (date: Date) => date.toLocaleDateString(), // OK
    parser: typeof window !== 'undefined' ? new DOMParser() : null
  };
};
```

### Database Access

```ts
// +page.server.ts - CORRECT
import { db } from '$lib/server/database';

export const load = async () => {
  const users = await db.query('SELECT * FROM users');
  return { users };
};
```

### Environment Variables

```ts
// +page.server.ts - Private env vars
import { DATABASE_URL, API_SECRET } from '$env/static/private';

// +page.js - Only public env vars
import { PUBLIC_API_URL } from '$env/static/public';
```

---

## Page Props Typing

SvelteKit generates types in `./$types` for full type safety with `$props()`.

### Basic Page Data Typing

```svelte
<!-- +page.svelte -->
<script lang="ts">
  import type { PageProps } from './$types';

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

<h1>{data.title}</h1>
```

### Page Data with Form Actions

```svelte
<script lang="ts">
  import type { PageProps } from './$types';

  let { data, form }: PageProps = $props();
</script>

{#if form?.success}
  <p class="success">{form.message}</p>
{/if}

{#if form?.error}
  <p class="error">{form.error}</p>
{/if}
```

### Layout Data Typing

```svelte
<!-- +layout.svelte -->
<script lang="ts">
  import type { LayoutProps } from './$types';

  let { data, children }: LayoutProps = $props();
</script>

<nav>
  {#if data.user}<span>Welcome, {data.user.name}</span>{/if}
</nav>

{@render children()}
```

### Corresponding Load Function

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

export const load: PageServerLoad = async () => {
  return {
    title: 'My Page',
    items: await fetchItems()
  };
};

export const actions: Actions = {
  default: async ({ request }) => {
    return { success: true, message: 'Saved successfully' };
  }
};
```

### Error Page

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

<h1>{page.status}: {page.error?.message}</h1>
```

---

## Form Actions Error Handling

SvelteKit distinguishes between validation errors (`fail()`) and unexpected errors (`throw error()`).

### Use fail() for Validation Errors

**WRONG:**
```ts
import { error } from '@sveltejs/kit';

export const actions = {
  default: async ({ request }) => {
    if (!email?.toString().includes('@')) {
      throw error(400, 'Invalid email'); // Shows error page!
    }
  }
};
```

**CORRECT:**
```ts
import { fail } from '@sveltejs/kit';

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const email = data.get('email')?.toString() ?? '';

    if (!email.includes('@')) {
      return fail(400, {
        error: 'Invalid email address',
        email // Return for repopulation
      });
    }

    return { success: true };
  }
};
```

### Use throw error() for Unexpected Errors

```ts
import { fail, error } from '@sveltejs/kit';

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();

    // Validation - use fail()
    if (!data.get('title')) {
      return fail(400, { error: 'Title is required' });
    }

    try {
      await database.save(data);
      return { success: true };
    } catch (e) {
      // Unexpected error - use throw
      throw error(500, 'Unable to save. Please try again later.');
    }
  }
};
```

### Structured Validation Responses

```ts
interface FormErrors {
  email?: string;
  password?: string;
}

export const actions = {
  register: async ({ request }) => {
    const data = await request.formData();
    const errors: FormErrors = {};

    if (!email) errors.email = 'Email is required';
    if (password.length < 8) errors.password = 'Password must be at least 8 characters';

    if (Object.keys(errors).length > 0) {
      return fail(400, { errors, values: { email } });
    }

    return { success: true };
  }
};
```

### Handle in Component

```svelte
<script lang="ts">
  import type { PageProps } from './$types';
  import { enhance } from '$app/forms';

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

<form method="POST" action="?/register" use:enhance>
  <label>
    Email
    <input name="email" value={form?.values?.email ?? ''} />
    {#if form?.errors?.email}
      <span class="error">{form.errors.email}</span>
    {/if}
  </label>
  <button type="submit">Register</button>
</form>
```

### Error Response Summary

| Situation | Function | Result |
|-----------|----------|--------|
| Missing field | `fail(400, {...})` | Form state preserved |
| Invalid format | `fail(400, {...})` | Form state preserved |
| Not found | `throw error(404, ...)` | Error page |
| Server crash | `throw error(500, ...)` | Error page |
| Auth required | `throw redirect(303, ...)` | Redirect |

---

## SSR State Isolation

Shared server state persists across requests, potentially leaking data between users.

### Dangerous: Module-Level State

**WRONG:**
```ts
// +page.server.ts
let currentUser = null; // SHARED ACROSS ALL REQUESTS!

export const load = async ({ locals }) => {
  currentUser = locals.user; // User B overwrites User A
  return { user: currentUser };
};
```

**CORRECT:**
```ts
export const load = async ({ locals }) => {
  return { user: locals.user }; // Each request gets its own locals
};
```

### Dangerous: Global Stores

**WRONG:**
```ts
// stores.svelte.ts
export const user = $state<User | null>(null); // Server-side singleton!
```

**CORRECT: Use locals and return data from load**
```ts
// +layout.server.ts
export const load = async ({ locals }) => {
  return { user: locals.user };
};
```

### Safe Patterns

**Pattern 1: Use locals**
```ts
// hooks.server.ts
export const handle = async ({ event, resolve }) => {
  event.locals.user = await authenticate(event);
  event.locals.requestId = crypto.randomUUID();
  return resolve(event);
};

// +page.server.ts
export const load = async ({ locals }) => {
  return { user: locals.user };
};
```

**Pattern 2: Context for Component Trees**
```svelte
<!-- +layout.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';
  import type { LayoutProps } from './$types';

  let { data, children }: LayoutProps = $props();

  setContext('user', {
    get current() { return data.user; }
  });
</script>

{@render children()}
```

**Pattern 3: Client-Only State**
```ts
// stores.svelte.ts
import { browser } from '$app/environment';

function createClientStore() {
  if (!browser) return { value: null };
  const state = $state({ value: null });
  return state;
}

export const clientState = createClientStore();
```

### SSR Safety Checklist

- [ ] No module-level `let` variables that store user data
- [ ] No global `$state` that gets set during SSR
- [ ] No singleton service classes with mutable state
- [ ] All user-specific data flows through `locals`
- [ ] All page data comes from load function returns
references/typescript.md›
# Svelte 5 TypeScript Reference

## Table of Contents
- [Props Typing](#props-typing)
- [Generic Components](#generic-components)

---

## Props Typing

`$props()` requires specific patterns for TypeScript typing.

### Basic Props Typing

**Inline typing:**
```svelte
<script lang="ts">
  let { name, count = 0 }: { name: string; count?: number } = $props();
</script>
```

**Interface typing (recommended):**
```svelte
<script lang="ts">
  interface Props {
    name: string;
    count?: number;
    disabled?: boolean;
  }

  let { name, count = 0, disabled = false }: Props = $props();
</script>
```

### Children and Snippets Typing

```svelte
<script lang="ts">
  import type { Snippet } from 'svelte';

  interface Props {
    children: Snippet;                    // Required children
    header?: Snippet;                     // Optional snippet
    row: Snippet<[data: RowData]>;       // Snippet with parameter
    cell?: Snippet<[value: string, index: number]>;  // Multiple params
  }

  interface RowData { id: number; name: string; }

  let { children, header, row, cell }: Props = $props();
</script>
```

### Callback Props Typing

```svelte
<script lang="ts">
  interface Props {
    value: string;
    onchange?: (value: string) => void;
    onsubmit?: (data: FormData) => Promise<void>;
    onclick?: (event: MouseEvent) => void;
  }

  let { value, onchange, onsubmit, onclick }: Props = $props();
</script>
```

### Rest Props with HTML Attributes

```svelte
<script lang="ts">
  import type { HTMLButtonAttributes } from 'svelte/elements';

  interface Props extends HTMLButtonAttributes {
    variant?: 'primary' | 'secondary';
    loading?: boolean;
  }

  let { variant = 'primary', loading = false, ...rest }: Props = $props();
</script>

<button class={variant} disabled={loading} {...rest}>
  {#if loading}Loading...{:else}{@render children?.()}{/if}
</button>
```

### Input Element Props

```svelte
<script lang="ts">
  import type { HTMLInputAttributes } from 'svelte/elements';

  interface Props extends Omit<HTMLInputAttributes, 'value'> {
    value?: string;
    label: string;
    error?: string;
  }

  let { value = $bindable(''), label, error, ...rest }: Props = $props();
</script>
```

### Union Type Props

```svelte
<script lang="ts">
  type ButtonVariant = 'primary' | 'secondary' | 'danger';
  type ButtonSize = 'sm' | 'md' | 'lg';

  interface Props {
    variant?: ButtonVariant;
    size?: ButtonSize;
    children: Snippet;
  }

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

<button class="{variant} {size}">{@render children()}</button>
```

### Discriminated Union Props

```svelte
<script lang="ts">
  type Props =
    | { type: 'link'; href: string; children: Snippet }
    | { type: 'button'; onclick: () => void; children: Snippet };

  let props: Props = $props();
</script>

{#if props.type === 'link'}
  <a href={props.href}>{@render props.children()}</a>
{:else}
  <button onclick={props.onclick}>{@render props.children()}</button>
{/if}
```

---

## Generic Components

Svelte 5 uses the `generics` attribute for type-safe reusable components.

### Basic Generic Component

```svelte
<!-- List.svelte -->
<script lang="ts" generics="T">
  interface Props {
    items: T[];
    children: import('svelte').Snippet<[item: T, index: number]>;
  }

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

<ul>
  {#each items as item, index}
    <li>{@render children(item, index)}</li>
  {/each}
</ul>

<!-- Usage -->
<script lang="ts">
  import List from './List.svelte';

  interface User { id: number; name: string; }
  let users: User[] = $state([{ id: 1, name: 'Alice' }]);
</script>

<List items={users}>
  {#snippet children(user, index)}
    <span>{index + 1}. {user.name}</span> <!-- Fully typed! -->
  {/snippet}
</List>
```

### Multiple Type Parameters

```svelte
<script lang="ts" generics="K, V">
  interface Props {
    entries: [K, V][];
    renderKey: import('svelte').Snippet<[key: K]>;
    renderValue: import('svelte').Snippet<[value: V]>;
  }

  let { entries, renderKey, renderValue }: Props = $props();
</script>

<dl>
  {#each entries as [key, value]}
    <dt>{@render renderKey(key)}</dt>
    <dd>{@render renderValue(value)}</dd>
  {/each}
</dl>
```

### Constrained Generics

```svelte
<script lang="ts" generics="T extends { id: string | number }">
  interface Props {
    items: T[];
    selected?: T;
    onselect?: (item: T) => void;
    children: import('svelte').Snippet<[item: T, isSelected: boolean]>;
  }

  let { items, selected, onselect, children }: Props = $props();
</script>

{#each items as item}
  <div
    class:selected={selected?.id === item.id}
    onclick={() => onselect?.(item)}
  >
    {@render children(item, selected?.id === item.id)}
  </div>
{/each}
```

### Generic with Default Type

```svelte
<script lang="ts" generics="T = Record<string, unknown>">
  interface Props {
    data: T[];
    columns: (keyof T)[];
  }

  let { data, columns }: Props = $props();
</script>
```

### Generic Select Component

```svelte
<script lang="ts" generics="T">
  interface Props {
    options: T[];
    value?: T;
    getLabel: (option: T) => string;
    getValue: (option: T) => string;
    onchange?: (selected: T | undefined) => void;
    placeholder?: string;
  }

  let {
    options,
    value = $bindable(),
    getLabel,
    getValue,
    onchange,
    placeholder = 'Select...'
  }: Props = $props();

  function handleChange(e: Event) {
    const selectedValue = (e.target as HTMLSelectElement).value;
    const selected = options.find(o => getValue(o) === selectedValue);
    value = selected;
    onchange?.(selected);
  }
</script>

<select onchange={handleChange}>
  <option value="">{placeholder}</option>
  {#each options as option}
    <option
      value={getValue(option)}
      selected={value && getValue(value) === getValue(option)}
    >
      {getLabel(option)}
    </option>
  {/each}
</select>
```

### Generic Async Component

```svelte
<script lang="ts" generics="T">
  import type { Snippet } from 'svelte';

  interface Props {
    promise: Promise<T>;
    loading?: Snippet;
    error?: Snippet<[error: Error]>;
    children: Snippet<[data: T]>;
  }

  let { promise, loading, error, children }: Props = $props();
</script>

{#await promise}
  {#if loading}{@render loading()}{:else}<p>Loading...</p>{/if}
{:then data}
  {@render children(data)}
{:catch err}
  {#if error}{@render error(err)}{:else}<p>Error: {err.message}</p>{/if}
{/await}
```
SKILL.md›
---
name: svelte5-best-practices
description: "Svelte 5 runes, snippets, SvelteKit patterns, and modern best practices for TypeScript and component development. Use when writing, reviewing, or refactoring Svelte 5 components and SvelteKit applications. Triggers on: Svelte components, runes ($state, $derived, $effect, $props, $bindable, $inspect), snippets ({#snippet}, {@render}), event handling, SvelteKit data loading, form actions, Svelte 4 to Svelte 5 migration, store to rune migration, slots to snippets migration, TypeScript props typing, generic components, SSR state isolation, performance optimization, or component testing."
license: MIT
metadata:
  author: ejirocodes
  version: '1.0.0'
---

# Svelte 5 Best Practices

## Quick Reference

| Topic | When to Use | Reference |
|-------|-------------|-----------|
| **Runes** | $state, $derived, $effect, $props, $bindable, $inspect | [runes.md](references/runes.md) |
| **Snippets** | Replacing slots, {#snippet}, {@render} | [snippets.md](references/snippets.md) |
| **Events** | onclick handlers, callback props, context API | [events.md](references/events.md) |
| **TypeScript** | Props typing, generic components | [typescript.md](references/typescript.md) |
| **Migration** | Svelte 4 to 5, stores to runes | [migration.md](references/migration.md) |
| **SvelteKit** | Load functions, form actions, SSR, page typing | [sveltekit.md](references/sveltekit.md) |
| **Performance** | Universal reactivity, avoiding over-reactivity, streaming | [performance.md](references/performance.md) |

## Essential Patterns

### Reactive State

```svelte
<script>
  let count = $state(0);           // Reactive state
  let doubled = $derived(count * 2); // Computed value
</script>
```

### Component Props

```svelte
<script>
  let { name, count = 0 } = $props();
  let { value = $bindable() } = $props(); // Two-way binding
</script>
```

### Snippets (replacing slots)

```svelte
<script>
  let { children, header } = $props();
</script>

{@render header?.()}
{@render children()}
```

### Event Handlers

```svelte
<!-- Svelte 5: use onclick, not on:click -->
<button onclick={() => count++}>Click</button>
```

### Callback Props (replacing createEventDispatcher)

```svelte
<script>
  let { onclick } = $props();
</script>

<button onclick={() => onclick?.({ data })}>Click</button>
```

## Common Mistakes

1. **Using `let` without `$state`** - Variables are not reactive without `$state()`
2. **Using `$effect` for derived values** - Use `$derived` instead
3. **Using `on:click` syntax** - Use `onclick` in Svelte 5
4. **Using `createEventDispatcher`** - Use callback props instead
5. **Using `<slot>`** - Use snippets with `{@render}`
6. **Forgetting `$bindable()`** - Required for `bind:` to work
7. **Setting module-level state in SSR** - Causes cross-request leaks
8. **Sequential awaits in load functions** - Use `Promise.all` for parallel requests