mcpField
S
svelte-development

svelte-development

github.com/manutej開発62435 回インストール40 週間前に更新
リポジトリ

svelte-development について

Svelte 5 を使う開発者向けのアプリ開発スキルです。リアクティビティ、コンポーネント、状態管理、UI 効果など、一般的な開発パターンを扱います。

主な機能

  1. 01Svelte 5 runes とリアクティビティ
  2. 02コンポーネント構造と合成
  3. 03ストアと共有状態
  4. 04ライフサイクルフックとバインディング
  5. 05トランジションとアニメーション
  6. 06フォーム、データ取得、実践方法

活用シーン

  1. 01リアクティブな Svelte UI の構築
  2. 02コンポーネント間の共有状態管理
  3. 03トランジションとリストアニメーションの追加
  4. 04フォーム検証とデータ取得の実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/manutej/luxor-claude-marketplace --skill svelte-development

インストール後、Claude Code で/svelte-developmentを入力して読み込まれたか確認します。

同梱ファイルEXAMPLES.mdREADME.md

詳細

Svelte 開発スキル

このスキルでは、Svelte の公式ドキュメントに基づき、リアクティビティルーン(Svelte 5)、コンポーネント、ストア、ライフサイクルフック、トランジション、アニメーションを使用して最新の Svelte アプリケーションを構築するための包括的なガイダンスを提供します。

このスキルを使用する場面

次のような場合に、このスキルを使用してください。

  • JavaScript のオーバーヘッドを最小限に抑え、高性能なウェブアプリケーションを構築する
  • リアクティブな UI を備えたシングルページアプリケーション(SPA)を作成する
  • コンパイル時の最適化を活用した、インタラクティブなユーザーインターフェースを開発する
  • バンドルサイズの小さな埋め込みウィジェットやコンポーネントを構築する
  • リアルタイムのダッシュボードやデータビジュアライゼーションを実装する
  • 優れたパフォーマンスを備えたプログレッシブウェブアプリ(PWA)を作成する
  • ネイティブのリアクティビティを備えたコンポーネントライブラリを開発する
  • SvelteKit を使用してサーバーサイドレンダリングアプリケーションを構築する
  • 仮想 DOM を使用するフレームワークから、コンパイル方式へ移行する
  • アクセシブルで高性能なウェブアプリケーションを作成する

コアコンセプト

ルーンによるリアクティビティ(Svelte 5)

Svelte 5 ではルーンが導入され、リアクティブな状態をより優れた TypeScript サポートと明確なセマンティクスで宣言できるようになりました。

$state ルーン:

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

  function increment() {
    count++;
  }

  function updateAge() {
    user.age++;
  }
</script>

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

<button on:click={updateAge}>
  {user.name} is {user.age} years old
</button>

$derived ルーン:

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

  // Complex derived values
  let users = $state([
    { name: 'Alice', active: true },
    { name: 'Bob', active: false },
    { name: 'Charlie', active: true }
  ]);

  let activeUsers = $derived(users.filter(u => u.active));
  let activeCount = $derived(activeUsers.length);
</script>

<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>Quadrupled: {quadrupled}</p>
<p>Active users: {activeCount}</p>

$effect ルーン:

<script>
  let count = $state(0);
  let name = $state('Alice');

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

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

    return () => {
      clearInterval(interval);
    };
  });

  // Conditional effects
  $effect(() => {
    if (count > 10) {
      console.log('Count exceeded 10!');
    }
  });
</script>

$props ルーン:

<script>
  // Type-safe props in Svelte 5
  let { name, age = 18, onClick } = $props();

  // With TypeScript
  interface Props {
    name: string;
    age?: number;
    onClick?: () => void;
  }

  let { name, age = 18, onClick }: Props = $props();
</script>

<div>
  <h2>{name}</h2>
  <p>Age: {age}</p>
  {#if onClick}
    <button on:click={onClick}>Click me</button>
  {/if}
</div>
コンポーネント

コンポーネントは Svelte アプリケーションを構成する基本要素です。各コンポーネントは、スクリプト、マークアップ、スタイルを含む単一のファイルです。

基本的なコンポーネント構造:

<script>
  // Component logic
  let name = $state('World');
  let count = $state(0);

  function handleClick() {
    count++;
  }
</script>


<div class="container">
  <h1>Hello {name}!</h1>
  <p>Count: {count}</p>
  <button on:click={handleClick}>Increment</button>
</div>


<style>
  .container {
    padding: 1rem;
    border: 1px solid #ccc;
    border-radius: 8px;
  }

  h1 {
    color: #ff3e00;
    font-size: 2rem;
  }

  button {
    background: #ff3e00;
    color: white;
    border: none;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    cursor: pointer;
  }

  button:hover {
    background: #ff5722;
  }
</style>

コンポーネントのプロパティ:


<script>
  let { title, description, imageUrl, onClick } = $props();
</script>

<div class="card" on:click={onClick}>
  {#if imageUrl}
    <img src={imageUrl} alt={title} />
  {/if}
  <h3>{title}</h3>
  <p>{description}</p>
</div>

<style>
  .card {
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 1rem;
    cursor: pointer;
    transition: transform 0.2s;
  }

  .card:hover {
    transform: translateY(-4px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  }

  img {
    width: 100%;
    border-radius: 4px;
  }
</style>

コンポーネントのイベント:


<script>
  import { createEventDispatcher } from 'svelte';

  let { variant = 'primary', disabled = false } = $props();

  const dispatch = createEventDispatcher();

  function handleClick() {
    dispatch('click', { timestamp: Date.now() });
  }
</script>

<button
  class="btn {variant}"
  {disabled}
  on:click={handleClick}
>
  <slot />
</button>

<style>
  .btn {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .primary {
    background: #ff3e00;
    color: white;
  }

  .secondary {
    background: #676778;
    color: white;
  }
</style>


<script>
  import Button from './Button.svelte';

  function handleButtonClick(event) {
    console.log('Clicked at:', event.detail.timestamp);
  }
</script>

<Button on:click={handleButtonClick}>Click me</Button>
<Button variant="secondary" on:click={handleButtonClick}>Secondary</Button>

スロットとコンポジション:


<script>
  let { isOpen = false, onClose } = $props();
</script>

{#if isOpen}
  <div class="modal-overlay" on:click={onClose}>
    <div class="modal-content" on:click|stopPropagation>
      <button class="close-btn" on:click={onClose}>×</button>

      <div class="modal-header">
        <slot name="header">
          <h2>Modal Title</h2>
        </slot>
      </div>

      <div class="modal-body">
        <slot>
          <p>Modal content goes here</p>
        </slot>
      </div>

      <div class="modal-footer">
        <slot name="footer">
          <button on:click={onClose}>Close</button>
        </slot>
      </div>
    </div>
  </div>
{/if}

<style>
  .modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
  }

  .modal-content {
    background: white;
    border-radius: 8px;
    padding: 2rem;
    max-width: 500px;
    width: 90%;
    position: relative;
  }

  .close-btn {
    position: absolute;
    top: 1rem;
    right: 1rem;
    background: none;
    border: none;
    font-size: 2rem;
    cursor: pointer;
  }
</style>


<script>
  import Modal from './Modal.svelte';
  let isModalOpen = $state(false);
</script>

<button on:click={() => isModalOpen = true}>Open Modal</button>

<Modal {isModalOpen} onClose={() => isModalOpen = false}>
  <svelte:fragment slot="header">
    <h2>Custom Title</h2>
  </svelte:fragment>

  <p>This is custom modal content.</p>

  <svelte:fragment slot="footer">
    <button on:click={() => isModalOpen = false}>Cancel</button>
    <button on:click={handleSave}>Save</button>
  </svelte:fragment>
</Modal>
ストア

ストアは、コンポーネント間で共有できる監視可能な値です。

書き込み可能なストア:

// stores.js
import { writable } from 'svelte/store';

export const count = writable(0);

export const user = writable({
  name: 'Guest',
  loggedIn: false
});

export const todos = writable([]);

// Custom store with methods
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();

コンポーネントでのストアの使用:

<script>
  import { count, user, counter } from './stores.js';

  // Auto-subscription with $
  $: console.log('Count changed:', $count);

  function increment() {
    count.update(n => n + 1);
  }

  function login() {
    user.set({ name: 'Alice', loggedIn: true });
  }
</script>

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

<p>Welcome, {$user.name}!</p>
{#if !$user.loggedIn}
  <button on:click={login}>Login</button>
{/if}

<p>Counter: {$counter}</p>
<button on:click={counter.increment}>+</button>
<button on:click={counter.decrement}>-</button>
<button on:click={counter.reset}>Reset</button>

読み取り専用ストア:

// stores.js
import { readable } from 'svelte/store';

export const time = readable(new Date(), (set) => {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  return () => clearInterval(interval);
});

export const mousePosition = readable({ x: 0, y: 0 }, (set) => {
  const handleMouseMove = (e) => {
    set({ x: e.clientX, y: e.clientY });
  };

  window.addEventListener('mousemove', handleMouseMove);

  return () => {
    window.removeEventListener('mousemove', handleMouseMove);
  };
});

派生ストア:

// stores.js
import { writable, derived } from 'svelte/store';

export const todos = writable([
  { id: 1, text: 'Buy milk', done: false },
  { id: 2, text: 'Walk dog', done: true },
  { id: 3, text: 'Code review', done: false }
]);

export const completedTodos = derived(
  todos,
  $todos => $todos.filter(t => t.done)
);

export const activeTodos = derived(
  todos,
  $todos => $todos.filter(t => !t.done)
);

export const todoStats = derived(
  todos,
  $todos => ({
    total: $todos.length,
    completed: $todos.filter(t => t.done).length,
    active: $todos.filter(t => !t.done).length
  })
);

// Derived from multiple stores
export const firstName = writable('Alice');
export const lastName = writable('Smith');

export const fullName = derived(
  [firstName, lastName],
  ([$firstName, $lastName]) => `${$firstName} ${$lastName}`
);

複雑なロジックを持つカスタムストア:

// stores/cart.js
import { writable, derived } from 'svelte/store';

function createCart() {
  const { subscribe, set, update } = writable([]);

  return {
    subscribe,
    addItem: (item) => update(items => {
      const existing = items.find(i => i.id === item.id);
      if (existing) {
        return items.map(i =>
          i.id === item.id
            ? { ...i, quantity: i.quantity + 1 }
            : i
        );
      }
      return [...items, { ...item, quantity: 1 }];
    }),
    removeItem: (id) => update(items =>
      items.filter(i => i.id !== id)
    ),
    updateQuantity: (id, quantity) => update(items =>
      items.map(i => i.id === id ? { ...i, quantity } : i)
    ),
    clear: () => set([])
  };
}

export const cart = createCart();

export const cartTotal = derived(
  cart,
  $cart => $cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

export const cartItemCount = derived(
  cart,
  $cart => $cart.reduce((count, item) => count + item.quantity, 0)
);
ライフサイクルフック

ライフサイクルフックを使用すると、コンポーネントのライフサイクルにおける特定のタイミングでコードを実行できます。

onMount:

<script>
  import { onMount } from 'svelte';

  let data = $state([]);
  let loading = $state(true);
  let error = $state(null);

  onMount(async () => {
    try {
      const response = await fetch('/api/data');
      if (!response.ok) throw new Error('Failed to fetch');
      data = await response.json();
    } catch (err) {
      error = err.message;
    } finally {
      loading = false;
    }
  });

  // onMount with cleanup
  onMount(() => {
    const interval = setInterval(() => {
      console.log('Tick');
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  });
</script>

{#if loading}
  <p>Loading...</p>
{:else if error}
  <p>Error: {error}</p>
{:else}
  <ul>
    {#each data as item}
      <li>{item.name}</li>
    {/each}
  </ul>
{/if}

onDestroy:

<script>
  import { onDestroy } from 'svelte';

  const subscription = eventSource.subscribe(data => {
    console.log(data);
  });

  onDestroy(() => {
    subscription.unsubscribe();
  });

  // Multiple cleanup operations
  onDestroy(() => {
    console.log('Component is being destroyed');
  });
</script>

beforeUpdate と afterUpdate:

<script>
  import { beforeUpdate, afterUpdate } from 'svelte';

  let div;
  let autoscroll = $state(true);

  beforeUpdate(() => {
    if (div) {
      const scrollableDistance = div.scrollHeight - div.offsetHeight;
      autoscroll = div.scrollTop > scrollableDistance - 20;
    }
  });

  afterUpdate(() => {
    if (autoscroll) {
      div.scrollTo(0, div.scrollHeight);
    }
  });
</script>

<div bind:this={div}>
  
</div>

tick:

<script>
  import { tick } from 'svelte';

  let text = $state('');
  let textarea;

  async function handleKeydown(event) {
    if (event.key === 'Tab') {
      event.preventDefault();

      const { selectionStart, selectionEnd, value } = textarea;
      text = value.slice(0, selectionStart) + '\t' + value.slice(selectionEnd);

      // Wait for DOM to update
      await tick();

      // Set cursor position
      textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
    }
  }
</script>

<textarea
  bind:value={text}
  bind:this={textarea}
  on:keydown={handleKeydown}
/>
トランジションとアニメーション

Svelte には、滑らかな UI 効果を実現するための組み込みトランジションとアニメーションが用意されています。

組み込みトランジション:

<script>
  import { fade, fly, slide, scale, blur } from 'svelte/transition';
  import { quintOut } from 'svelte/easing';

  let visible = $state(true);
</script>

<button on:click={() => visible = !visible}>Toggle</button>

{#if visible}
  <div transition:fade>Fades in and out</div>

  <div transition:fly={{ y: 200, duration: 500 }}>
    Flies in and out
  </div>

  <div transition:slide={{ duration: 300 }}>
    Slides in and out
  </div>

  <div transition:scale={{
    duration: 500,
    start: 0.5,
    easing: quintOut
  }}>
    Scales in and out
  </div>

  <div transition:blur={{ duration: 300 }}>
    Blurs in and out
  </div>
{/if}

イン・トランジションとアウト・トランジション:

<script>
  import { fade, fly } from 'svelte/transition';

  let visible = $state(true);
</script>

{#if visible}
  <div
    in:fly={{ y: -200, duration: 500 }}
    out:fade={{ duration: 200 }}
  >
    Different in/out transitions
  </div>
{/if}

カスタムトランジション:

<script>
  import { cubicOut } from 'svelte/easing';

  function typewriter(node, { speed = 1 }) {
    const valid = node.childNodes.length === 1 &&
                  node.childNodes[0].nodeType === Node.TEXT_NODE;

    if (!valid) return {};

    const text = node.textContent;
    const duration = text.length / (speed * 0.01);

    return {
      duration,
      tick: t => {
        const i = Math.trunc(text.length * t);
        node.textContent = text.slice(0, i);
      }
    };
  }

  function spin(node, { duration }) {
    return {
      duration,
      css: t => {
        const eased = cubicOut(t);
        return `
          transform: scale(${eased}) rotate(${eased * 360}deg);
          opacity: ${eased};
        `;
      }
    };
  }

  let visible = $state(false);
</script>

{#if visible}
  <p transition:typewriter={{ speed: 1 }}>
    This text will type out character by character
  </p>

  <div transition:spin={{ duration: 600 }}>
    Spinning!
  </div>
{/if}

アニメーション:

<script>
  import { flip } from 'svelte/animate';
  import { quintOut } from 'svelte/easing';

  let todos = $state([
    { id: 1, text: 'Buy milk' },
    { id: 2, text: 'Walk dog' },
    { id: 3, text: 'Code review' }
  ]);

  function shuffle() {
    todos = todos.sort(() => Math.random() - 0.5);
  }
</script>

<button on:click={shuffle}>Shuffle</button>

<ul>
  {#each todos as todo (todo.id)}
    <li animate:flip={{ duration: 300, easing: quintOut }}>
      {todo.text}
    </li>
  {/each}
</ul>

遅延トランジション:

<script>
  import { quintOut } from 'svelte/easing';
  import { crossfade } from 'svelte/transition';

  const [send, receive] = crossfade({
    duration: d => Math.sqrt(d * 200),
    fallback(node, params) {
      const style = getComputedStyle(node);
      const transform = style.transform === 'none' ? '' : style.transform;

      return {
        duration: 600,
        easing: quintOut,
        css: t => `
          transform: ${transform} scale(${t});
          opacity: ${t}
        `
      };
    }
  });

  let todos = $state([
    { id: 1, text: 'Buy milk', done: false },
    { id: 2, text: 'Walk dog', done: true }
  ]);

  function toggleDone(id) {
    todos = todos.map(t =>
      t.id === id ? { ...t, done: !t.done } : t
    );
  }
</script>

<div class="board">
  <div class="column">
    <h2>Todo</h2>
    {#each todos.filter(t => !t.done) as todo (todo.id)}
      <div
        class="card"
        in:receive={{ key: todo.id }}
        out:send={{ key: todo.id }}
        on:click={() => toggleDone(todo.id)}
      >
        {todo.text}
      </div>
    {/each}
  </div>

  <div class="column">
    <h2>Done</h2>
    {#each todos.filter(t => t.done) as todo (todo.id)}
      <div
        class="card"
        in:receive={{ key: todo.id }}
        out:send={{ key: todo.id }}
        on:click={() => toggleDone(todo.id)}
      >
        {todo.text}
      </div>
    {/each}
  </div>
</div>
バインディング

Svelte には、強力な双方向バインディング機能が用意されています。

入力バインディング:

<script>
  let name = $state('');
  let age = $state(0);
  let message = $state('');
  let selected = $state('');
  let checked = $state(false);
  let group = $state([]);
</script>


<input bind:value={name} placeholder="Enter name" />
<p>Hello {name}!</p>


<input type="number" bind:value={age} />
<p>Age: {age}</p>


<textarea bind:value={message}></textarea>
<p>Message length: {message.length}</p>


<select bind:value={selected}>
  <option value="red">Red</option>
  <option value="blue">Blue</option>
  <option value="green">Green</option>
</select>
<p>Selected: {selected}</p>


<input type="checkbox" bind:checked={checked} />
<p>Checked: {checked}</p>


<input type="checkbox" bind:group={group} value="apple" /> Apple
<input type="checkbox" bind:group={group} value="banana" /> Banana
<input type="checkbox" bind:group={group} value="orange" /> Orange
<p>Selected: {group.join(', ')}</p>

コンポーネントのバインディング:


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

<input bind:value />


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

<Input bind:value={name} />
<p>Name: {name}</p>

要素のバインディング:

<script>
  let canvas;
  let video;
  let div;

  let clientWidth = $state(0);
  let clientHeight = $state(0);
  let offsetWidth = $state(0);

  onMount(() => {
    const ctx = canvas.getContext('2d');
    // Draw on canvas
  });
</script>

<canvas bind:this={canvas} width={400} height={300}></canvas>

<video bind:this={video} bind:currentTime bind:duration bind:paused>
  <source src="video.mp4" />
</video>

<div bind:clientWidth bind:clientHeight bind:offsetWidth bind:this={div}>
  Size: {clientWidth} × {clientHeight}
</div>

contenteditable のバインディング:

<script>
  let html = $state('<p>Edit me!</p>');
</script>

<div contenteditable="true" bind:innerHTML={html}></div>

<pre>{html}</pre>

API リファレンス

ルーン(Svelte 5)

$state(initialValue)

  • リアクティブな状態を作成します
  • リアクティブ変数を返します
  • 値の変更によって更新が自動的にトリガーされます

$derived(expression)

  • 派生したリアクティブな値を作成します
  • 依存関係を自動的に追跡します
  • 依存関係が変化すると再計算します

$effect(callback)

  • 依存関係が変化したときに副作用を実行します
  • クリーンアップ関数を返すことができます
  • 依存関係を自動的に追跡します

$props()

  • コンポーネントのプロパティを宣言します
  • 分割代入とデフォルト値に対応しています
  • TypeScript による型安全性を備えています
ストア関数

writable(initialValue, start?)

  • 書き込み可能なストアを作成します
  • { subscribe, set, update } を返します
  • セットアップ用に start 関数を省略可能で指定できます

readable(initialValue, start)

  • 読み取り専用ストアを作成します
  • { subscribe } を返します
  • start 関数が必要です

derived(stores, callback, initialValue?)

  • 派生ストアを作成します
  • 1 つ以上のストアに依存します
  • 自動的に更新されます

get(store)

  • サブスクリプションを行わずに現在の値を取得します
  • 使用は控えめにしてください($store 構文を推奨)
ライフサイクル関数

onMount(callback)

  • コンポーネントの初回レンダリング後に実行されます
  • クリーンアップ関数を返すことができます
  • データの取得やサブスクリプションに適しています

onDestroy(callback)

  • コンポーネントが破棄される前に実行されます
  • クリーンアップ処理に使用します

beforeUpdate(callback)

  • DOM の更新前に実行されます
  • 以前の状態にアクセスできます

afterUpdate(callback)

  • DOM の更新後に実行されます
  • DOM の操作に適しています

tick()

  • 状態変更後に解決される Promise を返します
  • DOM の更新が完了することを保証します
トランジション関数

fade(node, params)

  • 要素をフェードイン/フェードアウトさせます
  • パラメーター:{ delay, duration, easing }

fly(node, params)

  • 要素を飛び込ませる/飛び出させます
  • パラメーター:{ delay, duration, easing, x, y, opacity }

slide(node, params)

  • 要素をスライドイン/スライドアウトさせます
  • パラメーター:{ delay, duration, easing }

scale(node, params)

  • 要素を拡大/縮小させて表示・非表示にします
  • パラメーター:{ delay, duration, easing, start, opacity }

blur(node, params)

  • 要素をぼかしながら表示・非表示にします
  • パラメーター:{ delay, duration, easing, amount, opacity }

crossfade(params)

  • 送信/受信のトランジションペアを作成します
  • リスト間で要素を移動させるのに適しています
アニメーション関数

flip(node, animation, params)

  • 位置の変化をアニメーション化します
  • each ブロックと併用します
  • パラメーター:{ delay, duration, easing }

ワークフローパターン

コンポーネントの構成

コンテナー/プレゼンターパターン:


<script>
  import TodoList from './TodoList.svelte';
  import { todos } from './stores.js';

  function addTodo(text) {
    todos.update(list => [...list, {
      id: Date.now(),
      text,
      done: false
    }]);
  }

  function toggleTodo(id) {
    todos.update(list => list.map(t =>
      t.id === id ? { ...t, done: !t.done } : t
    ));
  }

  function deleteTodo(id) {
    todos.update(list => list.filter(t => t.id !== id));
  }
</script>

<TodoList
  todos={$todos}
  onAdd={addTodo}
  onToggle={toggleTodo}
  onDelete={deleteTodo}
/>


<script>
  let { todos, onAdd, onToggle, onDelete } = $props();
  let newTodo = $state('');

  function handleSubmit() {
    if (newTodo.trim()) {
      onAdd(newTodo);
      newTodo = '';
    }
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <input bind:value={newTodo} placeholder="Add todo" />
  <button type="submit">Add</button>
</form>

<ul>
  {#each todos as todo}
    <li>
      <input
        type="checkbox"
        checked={todo.done}
        on:change={() => onToggle(todo.id)}
      />
      <span class:done={todo.done}>{todo.text}</span>
      <button on:click={() => onDelete(todo.id)}>Delete</button>
    </li>
  {/each}
</ul>

<style>
  .done {
    text-decoration: line-through;
    opacity: 0.6;
  }
</style>
状態管理

Context API パターン:


<script>
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';

  const user = writable({ name: 'Alice', role: 'admin' });
  const theme = writable('light');

  setContext('user', user);
  setContext('theme', theme);
</script>

<slot />


<script>
  import { getContext } from 'svelte';

  const user = getContext('user');
  const theme = getContext('theme');
</script>

<div class={$theme}>
  <p>Welcome, {$user.name}!</p>
  <p>Role: {$user.role}</p>
</div>
フォームのバリデーション

バリデーション付きフォーム:

<script>
  let formData = $state({
    email: '',
    password: '',
    confirmPassword: ''
  });

  let errors = $state({});
  let touched = $state({});
  let isSubmitting = $state(false);

  function validateEmail(email) {
    const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return re.test(email);
  }

  function validateForm() {
    const newErrors = {};

    if (!formData.email) {
      newErrors.email = 'Email is required';
    } else if (!validateEmail(formData.email)) {
      newErrors.email = 'Invalid email address';
    }

    if (!formData.password) {
      newErrors.password = 'Password is required';
    } else if (formData.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }

    if (formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = 'Passwords do not match';
    }

    return newErrors;
  }

  function handleBlur(field) {
    touched[field] = true;
    errors = validateForm();
  }

  async function handleSubmit() {
    touched = { email: true, password: true, confirmPassword: true };
    errors = validateForm();

    if (Object.keys(errors).length === 0) {
      isSubmitting = true;
      try {
        await submitForm(formData);
        // Success
      } catch (error) {
        errors.submit = error.message;
      } finally {
        isSubmitting = false;
      }
    }
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <div class="field">
    <label for="email">Email</label>
    <input
      id="email"
      type="email"
      bind:value={formData.email}
      on:blur={() => handleBlur('email')}
      class:error={touched.email && errors.email}
    />
    {#if touched.email && errors.email}
      <span class="error-message">{errors.email}</span>
    {/if}
  </div>

  <div class="field">
    <label for="password">Password</label>
    <input
      id="password"
      type="password"
      bind:value={formData.password}
      on:blur={() => handleBlur('password')}
      class:error={touched.password && errors.password}
    />
    {#if touched.password && errors.password}
      <span class="error-message">{errors.password}</span>
    {/if}
  </div>

  <div class="field">
    <label for="confirmPassword">Confirm Password</label>
    <input
      id="confirmPassword"
      type="password"
      bind:value={formData.confirmPassword}
      on:blur={() => handleBlur('confirmPassword')}
      class:error={touched.confirmPassword && errors.confirmPassword}
    />
    {#if touched.confirmPassword && errors.confirmPassword}
      <span class="error-message">{errors.confirmPassword}</span>
    {/if}
  </div>

  {#if errors.submit}
    <div class="error-message">{errors.submit}</div>
  {/if}

  <button type="submit" disabled={isSubmitting}>
    {isSubmitting ? 'Submitting...' : 'Submit'}
  </button>
</form>

<style>
  .field {
    margin-bottom: 1rem;
  }

  input.error {
    border-color: red;
  }

  .error-message {
    color: red;
    font-size: 0.875rem;
    margin-top: 0.25rem;
  }
</style>
データ取得

ローディング状態を伴うデータ取得:

<script>
  import { onMount } from 'svelte';

  let data = $state([]);
  let loading = $state(true);
  let error = $state(null);

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

    try {
      const response = await fetch('/api/data');
      if (!response.ok) throw new Error('Failed to fetch');
      data = await response.json();
    } catch (err) {
      error = err.message;
    } finally {
      loading = false;
    }
  }

  onMount(fetchData);
</script>

{#if loading}
  <div class="spinner">Loading...</div>
{:else if error}
  <div class="error">
    <p>Error: {error}</p>
    <button on:click={fetchData}>Retry</button>
  </div>
{:else}
  <ul>
    {#each data as item}
      <li>{item.name}</li>
    {/each}
  </ul>
{/if}

ベストプラクティス

1. リアクティブ処理には Runes を使用する(Svelte 5)

従来のリアクティブ宣言よりも Runes を優先します。


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

  $effect(() => {
    console.log(`Count: ${count}`);
  });
</script>


<script>
  let count = 0;
  $: doubled = count * 2;

  $: {
    console.log(`Count: ${count}`);
  }
</script>
2. コンポーネントの構成

コンポーネントの役割を絞り、単一の目的に集中させます。



<script>
  let { variant = 'primary', onClick } = $props();
</script>

<button class={variant} on:click={onClick}>
  <slot />
</button>


<script>
  // Button that also handles data fetching, validation, etc.
</script>
3. ストアの使用

共有状態にはストアを、コンポーネント固有のデータにはローカル状態を使用します。


<script>
  import { user } from './stores.js'; // Shared state
  let localCount = $state(0); // Component-specific
</script>


<script>
  import { count } from './stores.js'; // Only used in one component
</script>
4. アクセシビリティ

適切な ARIA 属性とキーボード操作への対応を必ず含めます。

<button
  on:click={handleClick}
  aria-label="Close dialog"
  aria-pressed={isPressed}
>
  Close
</button>

<input
  type="text"
  aria-label="Search"
  aria-describedby="search-help"
/>
<span id="search-help">Enter keywords to search</span>
5. パフォーマンスの最適化

リストにはキー付き each ブロックを使用します。


{#each items as item (item.id)}
  <Item {item} />
{/each}


{#each items as item}
  <Item {item} />
{/each}
6. TypeScript との統合

型安全性を確保するために TypeScript を使用します。

<script lang="ts">
  interface User {
    name: string;
    age: number;
    email?: string;
  }

  interface Props {
    user: User;
    onUpdate?: (user: User) => void;
  }

  let { user, onUpdate }: Props = $props();
</script>
7. CSS のスコープ化

Svelte のスコープ付きスタイルを活用します。

<style>
  /* Scoped to this component by default */
  .container {
    padding: 1rem;
  }

  /* Global styles when needed */
  :global(body) {
    margin: 0;
  }

  /* Combining scoped and global */
  .container :global(p) {
    color: blue;
  }
</style>
8. イベント修飾子

コードをすっきりさせるためにイベント修飾子を使用します。


<form on:submit|preventDefault={handleSubmit}>


<div on:click|stopPropagation={handleClick}>


<button on:click|once={handleClick}>


<div on:click|capture={handleClick}>


<div on:click|self={handleClick}>


<div on:scroll|passive={handleScroll}>


<div on:wheel|nonpassive={handleWheel}>
9. コンポーネント間の通信

子から親への通信にはイベントを使用します。


<script>
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  function notify() {
    dispatch('message', { text: 'Hello!' });
  }
</script>


<Child on:message={handleMessage} />
10. エラーバウンダリー

エラーを適切に処理します。

<script>
  import { onDestroy } from 'svelte';

  let error = $state(null);

  function handleError(err) {
    error = err.message;
    console.error(err);
  }

  // Global error handler
  const errorHandler = (event) => {
    handleError(event.error);
  };

  if (typeof window !== 'undefined') {
    window.addEventListener('error', errorHandler);
  }

  onDestroy(() => {
    if (typeof window !== 'undefined') {
      window.removeEventListener('error', errorHandler);
    }
  });
</script>

{#if error}
  <div class="error-boundary">
    <h2>Something went wrong</h2>
    <p>{error}</p>
    <button on:click={() => error = null}>Try again</button>
  </div>
{:else}
  <slot />
{/if}

まとめ

この Svelte 開発スキルでは、以下を扱います。

  1. Runes によるリアクティブ処理:$state、$derived、$effect、$props
  2. コンポーネント:構造、プロパティ、イベント、スロット
  3. ストア:書き込み可能、読み取り可能、派生、カスタムストア
  4. ライフサイクル:onMount、onDestroy、beforeUpdate、afterUpdate、tick
  5. トランジション:組み込みトランジションとカスタムトランジション
  6. アニメーション:FLIP アニメーション、crossfade
  7. バインディング:入力、コンポーネント、要素のバインディング
  8. ワークフローパターン:コンポーネントの構成、状態管理、フォーム、データ取得
  9. ベストプラクティス:パフォーマンス、アクセシビリティ、TypeScript、CSS のスコープ化
  10. 実践的なパターン:ToDo アプリ、モーダル、バリデーション付きフォーム

すべてのパターンは Runes を使用する Svelte 5 に基づいており、コンパイル時の最適化とリアクティブプログラミングに重点を置いた、最新の Svelte 開発手法を表しています。

コメント