Svelte 5で$effectをクラスのコンストラクタで使ったら「effect_orphan」エラーが出た話

Svelte 5で$effectをクラスのコンストラクタで使ったら「effect_orphan」エラーが出た話

Svelte 5でクラスのコンストラクタ内で`$effect`を使ったところ、Vercelデプロイ後に`effect_orphan`エラーが発生。原因と`$effect.root`による解決方法を紹介します。
2026.07.12

はじめに

SvelteKit + Svelte 5 で SPA を作っていたところ、ローカルの pnpm dev では問題なく動いていたのに、Vercel にデプロイしたら 500 Internal Error が発生しました。

ブラウザのコンソールを確認すると、以下のエラーが出ていました。

Error: https://svelte.dev/e/effect_orphan
    at Ee (CDll246k.js:1:1652)
    at Hn (CDll246k.js:1:23076)
    at Kn (CDll246k.js:1:23860)
    at new <anonymous> (Cy_cxSHz.js:1:1684)

本記事では、このエラーの原因と解決方法を紹介します。

前提・環境

  • Svelte 5.56.4
  • SvelteKit 2.68.0
  • @sveltejs/adapter-static 3.0.10(SPA モード)
  • Vercel(静的サイトとしてデプロイ)

エラーの原因

やっていたこと

localStorage と同期するステート管理を、Svelte 5 の Runes($state / $effect)を使って .svelte.ts ファイルに実装していました。

src/lib/state.svelte.ts
class QuizStateManager {
  bookmarks = $state<string[]>([]);
  history = $state<Record<string, QuestionHistory>>({});

  constructor() {
    const saved = loadFromStorage();
    this.bookmarks = saved.bookmarks;
    this.history = saved.history;

    // ❌ ここが問題
    $effect(() => {
      saveToStorage({
        bookmarks: this.bookmarks,
        history: this.history,
      });
    });
  }
  // ...
}

// モジュールスコープでインスタンス化
export const quizState = new QuizStateManager();

なぜエラーになるのか

Svelte 5 の $effect は、コンポーネントの初期化中(つまりコンポーネントのトップレベル)でのみ実行できるという制約があります。

上のコードでは QuizStateManager クラスをモジュールスコープで new しているため、どのコンポーネントの初期化コンテキストにも属していません。これが effect_orphan エラーの原因です。

Svelte 公式ドキュメントにも以下のように記載されています:

$effect can only be used in component initialization or inside another effect.

dev モードでは動いていた理由

pnpm dev(開発モード)では、Svelte はこのルール違反を警告として扱い、アプリの動作自体は止まりません。しかし、本番ビルド(pnpm build)ではこれがハードエラーとして扱われるため、デプロイ後に 500 エラーとして顕在化します。

解決方法

$effect.root で囲む

$effect.root は、コンポーネントのコンテキスト外でリアクティブなエフェクトを作成するための API です。これを使って $effect を囲むことで、スタンドアロンのリアクティブスコープを作ることができます。

src/lib/state.svelte.ts
class QuizStateManager {
  bookmarks = $state<string[]>([]);
  history = $state<Record<string, QuestionHistory>>({});

  constructor() {
    const saved = loadFromStorage();
    this.bookmarks = saved.bookmarks;
    this.history = saved.history;

    // ✅ $effect.root で囲む
    $effect.root(() => {
      $effect(() => {
        saveToStorage({
          bookmarks: this.bookmarks,
          history: this.history,
        });
      });
    });
  }
  // ...
}

export const quizState = new QuizStateManager();

この変更だけで、Vercel 上の 500 エラーが解消しました。

$effect.root とは

API 用途
$effect コンポーネント初期化中に使う。コンポーネントの破棄時に自動クリーンアップ
$effect.root コンポーネント外で使う。手動でクリーンアップが必要(戻り値の関数を呼ぶ)

$effect.root はクリーンアップ関数を返します。今回のケースではアプリ全体のライフサイクルと同じなので、明示的なクリーンアップは不要でした。もし特定のタイミングで購読を解除したい場合は、戻り値を保存して呼び出す必要があります。

const cleanup = $effect.root(() => {
  $effect(() => {
    // リアクティブな処理
  });
});

// 不要になったら
cleanup();

同じパターンに遭遇しやすいケース

以下のようなケースで同じエラーが起きやすいです。

  • シングルトンの状態管理クラスをモジュールスコープで new する
  • createXxx() ファクトリ関数をモジュールスコープで呼ぶ
  • ライブラリ初期化コード$effect を使う

いずれも「コンポーネントの外で $effect を使っている」という同じ原因です。

まとめ

  • Svelte 5 の $effect はコンポーネント初期化コンテキスト内でのみ使える
  • モジュールスコープのクラスやファクトリで使うと effect_orphan エラーになる
  • $effect.root で囲むことで解決できる
  • 開発モードでは気づきにくく、本番ビルドで顕在化するので注意

この記事をシェアする

関連記事