svelte/events - プログラマティックイベント管理

svelte/eventsモジュールは、Svelte 5でプログラム的にイベントハンドラを登録するためのユーティリティを提供します。テンプレート内のonclick属性ではなく、<script>ブロック内でイベントリスナーを管理する必要がある場面で使用します。

なぜ svelte/events が必要か

Svelte 5では、イベントハンドリングにイベント委譲(event delegation)を使用しています。onclickのような属性で宣言的に登録されたハンドラは、パフォーマンスのためにドキュメントルートで一括処理されます。

しかし、addEventListener()で直接登録したハンドラは委譲の対象外となり、実行順序が宣言的なハンドラと異なる場合があります。on()ヘルパーを使うことで、この順序の問題を解決できます。

on() ヘルパー関数

基本的な使い方

on()はイベントハンドラを登録し、解除関数を返します。

<script lang="ts">
  import { on } from 'svelte/events';

  let clickCount = $state(0);

  // windowにclickイベントを登録
  // 戻り値はイベント解除関数
  const cleanup = on(window, 'click', () => {
    clickCount++;
  });

  // 必要に応じて手動でイベント解除
  // cleanup();
</script>

<p>ページ全体のクリック回数: {clickCount}</p>
自動クリーンアップ

$effect内でon()を使用すると、effectのクリーンアップ時に自動的にイベントリスナーが解除されます。手動でcleanup()を呼ぶ必要はありません。

$effect との組み合わせ

on()$effectと組み合わせて使うのが最も一般的なパターンです。

<script lang="ts">
  import { on } from 'svelte/events';

  let mousePosition = $state({ x: 0, y: 0 });

  // effectが破棄されると自動的にイベントリスナーも解除される
  $effect(() => {
    return on(window, 'mousemove', (event: MouseEvent) => {
      mousePosition = { x: event.clientX, y: event.clientY };
    });
  });
</script>

<p>マウス位置: ({mousePosition.x}, {mousePosition.y})</p>
returnパターン

$effectのreturnにon()の戻り値をそのまま返すと、effectの破棄時にイベントリスナーが自動解除されます。on()が返すのは() => void型のクリーンアップ関数です。

DOM要素への登録

windowdocumentだけでなく、任意のHTML要素に対しても使用できます。

<script lang="ts">
  import { on } from 'svelte/events';

  let el: HTMLDivElement;
  let isHovering = $state(false);

  $effect(() => {
    // DOM要素への参照が確立された後にイベント登録
    const offEnter = on(el, 'mouseenter', () => {
      isHovering = true;
    });

    const offLeave = on(el, 'mouseleave', () => {
      isHovering = false;
    });

    // 両方のリスナーをクリーンアップ
    return () => {
      offEnter();
      offLeave();
    };
  });
</script>

<div bind:this={el} class:highlight={isHovering}>
  ホバーしてください
</div>

イベントオプション

addEventListenerと同じオプションを第4引数に渡せます。

<script lang="ts">
  import { on } from 'svelte/events';

  $effect(() => {
    // once: 一度だけ実行
    const offOnce = on(window, 'resize', () => {
      console.log('初回リサイズのみ検知');
    }, { once: true });

    // capture: キャプチャフェーズで実行
    const offCapture = on(document, 'click', (event: MouseEvent) => {
      console.log('キャプチャフェーズ:', event.target);
    }, { capture: true });

    // passive: スクロールパフォーマンス向上
    const offScroll = on(window, 'scroll', () => {
      console.log('パッシブスクロール');
    }, { passive: true });

    return () => {
      offOnce();
      offCapture();
      offScroll();
    };
  });
</script>

TypeScript での型安全性

on()は対象要素ごとに適切なイベント型を自動推論します。

import { on } from 'svelte/events';

// Window — WindowEventMapから型推論
on(window, 'resize', (event) => {
  // event: UIEvent & { currentTarget: Window }
  console.log(event.currentTarget.innerWidth);
});

// Document — DocumentEventMapから型推論
on(document, 'visibilitychange', (event) => {
  // event: Event & { currentTarget: Document }
  console.log(document.visibilityState);
});

// HTMLElement — HTMLElementEventMapから型推論
const button = document.querySelector('button')!;
on(button, 'click', (event) => {
  // event: MouseEvent & { currentTarget: HTMLButtonElement }
  console.log(event.currentTarget.textContent);
});

// カスタムイベント — EventTarget + string型
const target = new EventTarget();
on(target, 'custom-event', (event) => {
  // event: Event(汎用型)
  console.log(event.type);
});

currentTarget の型保証

on()currentTargetプロパティを正確な要素型に絞り込みます。これにより、addEventListenerでは得られない型安全性が実現します。

import { on } from 'svelte/events';

// addEventListenerの場合
window.addEventListener('click', (event) => {
  // event.currentTarget は EventTarget | null 型
  // キャストが必要
});

// on()の場合
on(window, 'click', (event) => {
  // event.currentTarget は Window 型(キャスト不要)
  event.currentTarget.scrollTo(0, 0);
});

実践例

キーボードショートカット

<script lang="ts">
  import { on } from 'svelte/events';

  let message = $state('');

  $effect(() => {
    return on(window, 'keydown', (event: KeyboardEvent) => {
      // Ctrl+S(またはCmd+S)でセーブ処理
      if ((event.ctrlKey || event.metaKey) && event.key === 's') {
        event.preventDefault();
        message = '保存しました!';
        setTimeout(() => { message = ''; }, 2000);
      }

      // Escape でモーダルクローズ
      if (event.key === 'Escape') {
        message = 'Escapeが押されました';
      }
    });
  });
</script>

{#if message}
  <div class="notification">{message}</div>
{/if}
<p>Ctrl+Sまたは Escapeを押してみてください</p>

スクロール位置の追跡

<script lang="ts">
  import { on } from 'svelte/events';

  let scrollY = $state(0);
  let isScrolledDown = $derived(scrollY > 100);

  $effect(() => {
    return on(window, 'scroll', () => {
      scrollY = window.scrollY;
    }, { passive: true });
  });
</script>

{#if isScrolledDown}
  <button onclick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
    トップへ戻る
  </button>
{/if}

ResizeObserver の代替としての使用

<script lang="ts">
  import { on } from 'svelte/events';

  let windowSize = $state({ width: 0, height: 0 });

  $effect(() => {
    // 初期値を設定
    windowSize = {
      width: window.innerWidth,
      height: window.innerHeight
    };

    return on(window, 'resize', () => {
      windowSize = {
        width: window.innerWidth,
        height: window.innerHeight
      };
    });
  });
</script>

<p>ウィンドウサイズ: {windowSize.width} x {windowSize.height}</p>

addEventListener との違い

特徴on()addEventListener()
イベント委譲との順序保証される保証されない
戻り値クリーンアップ関数なし(removeEventListenerが必要)
TypeScript型推論currentTargetが正確currentTargetEventTarget \| null
Svelte統合$effectとの自然な連携手動管理が必要
addEventListenerとの順序問題

addEventListener()で直接登録したハンドラは、Svelteのイベント委譲システム(onclick属性)とは異なるタイミングで実行される可能性があります。同じ要素で両方を使う場合は、on()を使って順序を保証してください。

createSubscriber との関連

on()svelte/reactivitycreateSubscriber()と組み合わせて、外部イベントソースをリアクティブシステムに統合できます。実際にsvelte/reactivityMediaQueryクラスは内部でon()を使用しています。

// MediaQueryの内部実装イメージ
import { createSubscriber } from 'svelte/reactivity';
import { on } from 'svelte/events';

class MediaQuery {
  #query: MediaQueryList;
  #subscribe: () => void;

  constructor(query: string) {
    this.#query = window.matchMedia(`(${query})`);

    this.#subscribe = createSubscriber((update) => {
      // on()でchangeイベントを監視し、update()で再評価をトリガー
      const off = on(this.#query, 'change', update);
      return () => off();
    });
  }

  get current(): boolean {
    this.#subscribe(); // effectコンテキストでリアクティブに
    return this.#query.matches;
  }
}
MediaQueryクラスについて

MediaQueryクラス自体はsvelte/reactivityモジュールからインポートします。詳しくは組み込みリアクティブクラスを参照してください。

よくある間違い

$effect 外での使用

<script lang="ts">
  import { on } from 'svelte/events';

  // ❌ $effect外で使うとコンポーネント破棄時にリークする可能性
  on(window, 'resize', () => {
    console.log('リサイズ');
  });

  // ✅ $effect内で使い、自動クリーンアップを活用
  $effect(() => {
    return on(window, 'resize', () => {
      console.log('リサイズ');
    });
  });
</script>

テンプレートイベントとの重複

<script lang="ts">
  import { on } from 'svelte/events';

  let el: HTMLButtonElement;
  let count = $state(0);

  // ❌ テンプレートのonclickと重複する必要はない
  $effect(() => {
    return on(el, 'click', () => { count++; });
  });
</script>

<!-- テンプレートで宣言的に書ける場合はそちらが推奨 -->
<!-- ✅ シンプルなケースはonclick属性で十分 -->
<button bind:this={el} onclick={() => count++}>
  カウント: {count}
</button>
使い分けの指針

テンプレート内のonclick等の属性で対応できる場合は、宣言的な書き方を優先してください。on()は、window/documentへのグローバルイベント、動的に変わるイベント対象、$effect内でのプログラム的な管理が必要な場面で使用します。

まとめ

svelte/eventson()ヘルパーは、プログラム的なイベント管理をSvelte 5のリアクティブシステムと統合するための重要なツールです。addEventListenerの代わりに使用することで、イベント委譲との順序保証、自動クリーンアップ、TypeScript型安全性といった恩恵を得られます。テンプレートの宣言的イベントハンドリングと適切に使い分けることで、堅牢なイベント管理が実現できます。

次のステップ