T-CREATOR

Svelte ストアエラー「store is not a function」を解決:writable/derived の落とし穴

Svelte ストアエラー「store is not a function」を解決:writable/derived の落とし穴

Svelte でストアを使った状態管理を実装していると、突然「store is not a function」というエラーに遭遇することがあります。このエラーは、初心者だけでなく経験者でも陥りやすい落とし穴の一つです。

この記事では、writable や derived などのストアを使う際に発生する「store is not a function」エラーの原因と解決方法を徹底解説します。実際のコード例を交えながら、エラーを回避するベストプラクティスもご紹介しますので、ぜひ最後までお読みください。

背景

Svelte ストアの基本的な仕組み

Svelte におけるストアは、コンポーネント間で状態を共有するための強力な仕組みです。ストアを使うことで、親子関係にないコンポーネント同士でも簡単にデータをやり取りできます。

Svelte が提供する主なストア関数には、以下の 3 種類があります。

#ストア関数説明用途
1writable読み書き可能なストア値の設定・更新が必要な状態管理
2readable読み取り専用のストア定期的に更新される値やタイマーなど
3derived他のストアから派生したストア計算済みの値や複数ストアの組み合わせ

以下の図は、Svelte アプリケーション内でストアがどのように機能するかを示しています。

mermaidflowchart TB
  comp1["コンポーネントA"] -->|subscribe| store1["writable ストア"]
  comp2["コンポーネントB"] -->|subscribe| store1
  comp3["コンポーネントC"] -->|subscribe| store1
  store1 -->|通知| comp1
  store1 -->|通知| comp2
  store1 -->|通知| comp3
  comp1 -->|set/update| store1
  comp2 -->|set/update| store1

図で理解できる要点:

  • 複数のコンポーネントが同じストアを購読(subscribe)できる
  • ストアの値が変更されると、すべての購読者に自動的に通知される
  • writable ストアは任意のコンポーネントから更新可能

writable ストアの基本的な使い方

まずは、writable ストアの基本的な作成方法を見ていきましょう。

javascript// stores.js - ストアファイル
import { writable } from 'svelte/store';

// 初期値を指定してストアを作成
export const count = writable(0);

このコードでは、初期値 0 を持つ writable ストアを作成しています。writable 関数は Svelte の組み込み関数で、読み書き可能なストアオブジェクトを返します。

Svelte コンポーネント内でストアを使う場合、$ プレフィックスを付けることで自動購読できます。

javascript<script>
  // ストアをインポート
  import { count } from './stores.js';

  // ストアの値を更新する関数
  function increment() {
    count.update(n => n + 1);
  }
</script>

<!-- $ プレフィックスで自動購読 -->
<p>カウント: {$count}</p>
<button on:click={increment}>+1</button>

$count という記法により、Svelte が自動的にストアの購読と購読解除を管理してくれます。これにより、メモリリークの心配なく簡潔なコードが書けます。

derived ストアの基本的な使い方

derived ストアは、他のストアから計算された値を提供するストアです。元のストアが更新されると、derived ストアも自動的に再計算されます。

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

// 元となるストア
export const count = writable(0);

// count ストアから派生したストア
export const doubled = derived(
  count,
  ($count) => $count * 2
);

このコードでは、count ストアの値を 2 倍にした doubled ストアを作成しています。derived 関数の第一引数は元のストア、第二引数は変換関数です。

複数のストアから派生させることもできます。

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

// 複数の元ストア
export const firstName = writable('太郎');
export const lastName = writable('山田');

// 複数ストアから派生
export const fullName = derived(
  [firstName, lastName],
  ([$firstName, $lastName]) => `${$lastName} ${$firstName}`
);

複数のストアを配列で渡すと、derived 関数は各ストアの値を配列として受け取ります。この例では、姓と名を組み合わせてフルネームを生成しています。

課題

エラーが発生する典型的なパターン

「store is not a function」エラーは、主に以下のような場面で発生します。

エラーコード:TypeError

phpTypeError: store is not a function
    at get_store_value (index.mjs:1968:23)
    at instance (Component.svelte:42:18)

このエラーメッセージは、Svelte がストアとして扱おうとしたオブジェクトに subscribe メソッドが存在しない場合に表示されます。

以下の図は、エラーが発生する主なパターンを示しています。

mermaidflowchart TB
  start["ストア使用開始"] --> check1{"正しいインポート?"}
  check1 -->|No| error1["エラー:<br/>store is not a function"]
  check1 -->|Yes| check2{"$ 記法を正しく使用?"}
  check2 -->|No| error2["エラー:<br/>store is not a function"]
  check2 -->|Yes| check3{"derived に<br/>ストアを渡してる?"}
  check3 -->|No| error3["エラー:<br/>store is not a function"]
  check3 -->|Yes| success["正常動作"]

図で理解できる要点:

  • エラーの原因は主に 3 つのチェックポイントで発生
  • インポート方法、使用方法、derived の引数が主な確認ポイント
  • 順序立てて確認することでエラーを特定しやすい

パターン 1:誤ったインポート方法

最も多いのが、ストアの値を直接インポートしようとするケースです。

javascript<script>
  // ❌ 誤り:ストアの値を取得しようとしている import{' '}
  {$count} from './stores.js'; console.log($count); //
  エラーが発生
</script>

このコードは、stores.js から $count という名前のエクスポートを探そうとしますが、実際にエクスポートされているのは count というストアオブジェクトです。$ 記法はコンポーネント内でのみ有効で、インポート時には使えません。

パターン 2:ストア関数の誤用

writable や derived 関数を誤って二重に呼び出すケースも見られます。

javascript<script>
  import {writable} from 'svelte/store'; import {count} from
  './stores.js'; // ❌ 誤り:既にストアであるものを再度
  writable で包んでいる const myCount = writable(count);
  console.log($myCount); // エラーが発生
</script>

count は既に writable ストアとして作成されているため、それを再度 writable で包む必要はありません。この場合、myCount はストアを含むストアという入れ子構造になり、正しく動作しません。

パターン 3:derived ストアへの値渡し

derived ストアに通常の値を渡してしまうケースも頻発します。

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

export const count = writable(5);

// ❌ 誤り:ストアの値を直接渡している
export const doubled = derived(5, ($count) => $count * 2);

derived 関数の第一引数には、ストアオブジェクトを渡す必要があります。上記のコードでは、数値 5 を渡しているため、Svelte はそれを subscribe しようとしてエラーが発生します。

パターン 4:コンポーネント外での $ 記法使用

コンポーネント外のファイルで $ 記法を使おうとするケースです。

javascript// utils.js(コンポーネントではない)
import { count } from './stores.js';

// ❌ 誤り:コンポーネント外で $ 記法を使用
export function getDoubledCount() {
  return $count * 2; // エラーが発生
}

$ 記法は Svelte コンポーネント(.svelte ファイル)の <script> タグ内でのみ使用できます。通常の JavaScript ファイルでは使えません。

解決策

パターン別の正しい実装方法

それぞれのエラーパターンに対する正しい実装方法を見ていきましょう。

解決策 1:正しいインポート方法

ストアは常にストアオブジェクトとしてインポートし、コンポーネント内で $ 記法を使います。

javascript<script>
  // ✅ 正しい:ストアオブジェクトをインポート
  import { count } from './stores.js';

  // コンポーネント内で $ 記法を使用
  console.log($count); // 正常に動作
</script>

<p>カウント: {$count}</p>

このコードでは、count ストアをそのままインポートし、コンポーネント内で $count として値にアクセスしています。これが最も基本的で推奨される方法です。

インポートとエクスポートの正しい対応関係を表にまとめました。

#エクスポート側(stores.js)インポート側(Component.svelte)結果
1export const count = writable(0)import { count } from '.​/​stores.js'✅ 正しい
2export const count = writable(0)import { $count } from '.​/​stores.js'❌ エラー
3export const count = writable(0)import count from '.​/​stores.js'❌ エラー(default export ではない)

解決策 2:get 関数を使った値の取得

コンポーネント外でストアの値を取得する必要がある場合は、get 関数を使います。

javascript// utils.js
import { get } from 'svelte/store';
import { count } from './stores.js';

// ✅ 正しい:get 関数を使ってストアの値を取得
export function getDoubledCount() {
  const currentCount = get(count);
  return currentCount * 2;
}

get 関数は、ストアの現在の値を一度だけ取得します。ただし、get は reactive ではないため、ストアの値が変更されても自動的に再計算されません。

以下は、コンポーネント内とコンポーネント外でのストア利用方法の違いを示した図です。

mermaidflowchart LR
  subgraph component ["コンポーネント内(.svelte)"]
    dollar["$ 記法<br/>$count"]
  end

  subgraph outside ["コンポーネント外(.js)"]
    getFunc["get 関数<br/>get(count)"]
    subscribe["subscribe メソッド<br/>count.subscribe(...)"]
  end

  store["writable ストア<br/>count"] --> dollar
  store --> getFunc
  store --> subscribe

  dollar -->|自動購読・購読解除| reactive["リアクティブ"]
  getFunc -->|一度だけ取得| single["非リアクティブ"]
  subscribe -->|手動購読・購読解除| manual["リアクティブ<br/>(手動管理)"]

図で理解できる要点:

  • コンポーネント内では $ 記法が最も簡潔で推奨される
  • コンポーネント外では get 関数か subscribe メソッドを使用
  • リアクティブ性が必要かどうかで使い分ける

解決策 3:subscribe メソッドの活用

ストアの値をリアクティブに監視する必要がある場合は、subscribe メソッドを使います。

javascript// utils.js
import { count } from './stores.js';

// ✅ 正しい:subscribe でリアクティブに監視
export function watchCount(callback) {
  // subscribe は購読解除関数を返す
  const unsubscribe = count.subscribe((value) => {
    callback(value);
  });

  // クリーンアップ用に購読解除関数を返す
  return unsubscribe;
}

このコードでは、subscribe メソッドを使ってストアの変更を監視しています。subscribe は購読解除関数を返すため、それを呼び出すことでメモリリークを防げます。

実際の使用例を見てみましょう。

javascript<script>
  import { onDestroy } from 'svelte';
  import { watchCount } from './utils.js';

  let displayValue = 0;

  // ストアの変更を監視
  const unsubscribe = watchCount(value => {
    displayValue = value * 2;
  });

  // コンポーネント破棄時に購読解除
  onDestroy(() => {
    unsubscribe();
  });
</script>

<p>2倍の値: {displayValue}</p>

コンポーネントが破棄される際に onDestroy フックで購読を解除することで、メモリリークを防いでいます。

解決策 4:derived ストアの正しい使い方

derived ストアには、必ずストアオブジェクトを渡します。

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

// 元のストア
export const count = writable(5);

// ✅ 正しい:ストアオブジェクトを渡す
export const doubled = derived(
  count,
  ($count) => $count * 2
);

derived の第一引数に count ストアを渡し、第二引数の関数内で $count として値にアクセスしています。これにより、count が更新されるたびに doubled も自動的に再計算されます。

複数のストアから派生させる場合も同様です。

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

export const price = writable(1000);
export const quantity = writable(3);
export const taxRate = writable(0.1);

// ✅ 正しい:複数のストアオブジェクトを配列で渡す
export const totalPrice = derived(
  [price, quantity, taxRate],
  ([$price, $quantity, $taxRate]) => {
    const subtotal = $price * $quantity;
    const tax = subtotal * $taxRate;
    return subtotal + tax;
  }
);

このコードでは、価格、数量、税率の 3 つのストアから、税込み合計金額を計算しています。いずれかのストアが更新されると、自動的に合計金額も再計算されます。

解決策 5:カスタムストアの実装

より複雑なロジックが必要な場合は、カスタムストアを作成できます。

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

// カスタムストアを作成する関数
function createCounter() {
  // 内部で writable ストアを作成
  const { subscribe, set, update } = writable(0);

  return {
    subscribe, // subscribe メソッドは必須
    increment: () => update((n) => n + 1),
    decrement: () => update((n) => n - 1),
    reset: () => set(0),
  };
}

// ✅ 正しい:カスタムストアをエクスポート
export const counter = createCounter();

カスタムストアは、writable ストアをラップして、独自のメソッドを追加したものです。subscribe メソッドを公開することで、Svelte がストアとして認識できます。

コンポーネントでの使用方法は通常のストアと同じです。

javascript<script>
  import { counter } from './stores.js';
</script>

<p>カウント: {$counter}</p>

<button on:click={counter.increment}>+1</button>
<button on:click={counter.decrement}>-1</button>
<button on:click={counter.reset}>リセット</button>

カスタムメソッドを使うことで、ストアの操作がより直感的で読みやすくなります。

具体例

実践例 1:ショッピングカート機能

実際のアプリケーションでよくある、ショッピングカート機能を実装してみましょう。

まず、ストアファイルを作成します。

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

// カート内の商品を管理するストア
function createCart() {
  const { subscribe, set, update } = writable([]);

  return {
    subscribe,

    // 商品を追加
    addItem: (product) =>
      update((items) => {
        const existingItem = items.find(
          (item) => item.id === product.id
        );

        if (existingItem) {
          // 既存商品の数量を増やす
          return items.map((item) =>
            item.id === product.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          );
        } else {
          // 新規商品を追加
          return [...items, { ...product, quantity: 1 }];
        }
      }),

    // 商品を削除
    removeItem: (productId) =>
      update((items) =>
        items.filter((item) => item.id !== productId)
      ),

    // カートをクリア
    clear: () => set([]),
  };
}

export const cart = createCart();

このカスタムストアは、商品の追加、削除、クリア機能を持っています。既存の商品を追加した場合は数量を増やし、新規の商品は配列に追加します。

次に、derived ストアで合計金額を計算します。

javascript// stores/cart.js(続き)

// カート内の合計金額を計算する derived ストア
export const totalPrice = derived(cart, ($cart) => {
  return $cart.reduce((total, item) => {
    return total + item.price * item.quantity;
  }, 0);
});

// カート内の商品数を計算する derived ストア
export const itemCount = derived(cart, ($cart) => {
  return $cart.reduce((count, item) => {
    return count + item.quantity;
  }, 0);
});

これらの derived ストアは、カートの内容が変更されるたびに自動的に再計算されます。

以下は、ショッピングカートのデータフローを示した図です。

mermaidflowchart TB
  user["ユーザー操作"] -->|商品追加| addItem["cart.addItem()"]
  user -->|商品削除| removeItem["cart.removeItem()"]

  addItem --> cartStore["cart ストア<br/>[商品配列]"]
  removeItem --> cartStore

  cartStore --> derived1["totalPrice<br/>(derived)"]
  cartStore --> derived2["itemCount<br/>(derived)"]

  derived1 --> display1["合計金額表示"]
  derived2 --> display2["商品数バッジ"]
  cartStore --> display3["カート一覧表示"]

図で理解できる要点:

  • カスタムストアが商品の追加・削除を処理
  • derived ストアが自動的に合計金額と商品数を計算
  • コンポーネントは計算済みの値を表示するだけ

コンポーネントでの使用例を見てみましょう。

javascript<script>
  // ストアをインポート
  import { cart, totalPrice, itemCount } from './stores/cart.js';

  // サンプル商品
  const sampleProduct = {
    id: 1,
    name: 'ノートパソコン',
    price: 89800
  };

  function handleAddToCart() {
    cart.addItem(sampleProduct);
  }
</script>

<div class="cart-summary">
  <h2>カート概要</h2>
  <p>商品数: {$itemCount}点</p>
  <p>合計金額: ¥{$totalPrice.toLocaleString()}</p>
</div>

<button on:click={handleAddToCart}>
  カートに追加
</button>

<div class="cart-items">
  {#each $cart as item (item.id)}
    <div class="item">
      <span>{item.name}</span>
      <span>¥{item.price.toLocaleString()}</span>
      <span>×{item.quantity}</span>
      <button on:click={() => cart.removeItem(item.id)}>
        削除
      </button>
    </div>
  {/each}
</div>

{#if $itemCount > 0}
  <button on:click={() => cart.clear()}>
    カートをクリア
  </button>
{/if}

このコンポーネントでは、$cart$totalPrice$itemCount という 3 つのストアを使っています。すべて $ 記法で自動購読されるため、値が変更されると自動的に再レンダリングされます。

実践例 2:フォームバリデーション

フォームのバリデーション状態を管理するストアを実装してみましょう。

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

// フォームの各フィールドを管理
export const email = writable('');
export const password = writable('');
export const passwordConfirm = writable('');

まず、各フィールド用の writable ストアを作成します。これらのストアには、ユーザーが入力した値が格納されます。

次に、各フィールドのバリデーションを行う derived ストアを作成します。

javascript// stores/form.js(続き)

// メールアドレスのバリデーション
export const emailValid = derived(email, ($email) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test($email);
});

// パスワードのバリデーション(8文字以上)
export const passwordValid = derived(
  password,
  ($password) => {
    return $password.length >= 8;
  }
);

// パスワード確認のバリデーション
export const passwordConfirmValid = derived(
  [password, passwordConfirm],
  ([$password, $passwordConfirm]) => {
    return (
      $password === $passwordConfirm &&
      $passwordConfirm !== ''
    );
  }
);

各 derived ストアは、対応するフィールドの値をチェックして、バリデーション結果を true/false で返します。

最後に、全体のフォームが有効かどうかを判定する derived ストアを作成します。

javascript// stores/form.js(続き)

// フォーム全体のバリデーション
export const formValid = derived(
  [emailValid, passwordValid, passwordConfirmValid],
  ([
    $emailValid,
    $passwordValid,
    $passwordConfirmValid,
  ]) => {
    return (
      $emailValid && $passwordValid && $passwordConfirmValid
    );
  }
);

この formValid ストアは、すべてのフィールドが有効な場合のみ true を返します。

以下は、フォームバリデーションの構造を示した図です。

mermaidflowchart TB
  subgraph inputs ["入力フィールド(writable)"]
    email["email"]
    password["password"]
    passwordConfirm["passwordConfirm"]
  end

  subgraph validation ["バリデーション(derived)"]
    emailValid["emailValid"]
    passwordValid["passwordValid"]
    passwordConfirmValid["passwordConfirmValid"]
  end

  subgraph result ["結果(derived)"]
    formValid["formValid"]
  end

  email --> emailValid
  password --> passwordValid
  password --> passwordConfirmValid
  passwordConfirm --> passwordConfirmValid

  emailValid --> formValid
  passwordValid --> formValid
  passwordConfirmValid --> formValid

  formValid --> submit["送信ボタン<br/>有効/無効"]

図で理解できる要点:

  • 入力値から段階的にバリデーション結果を導出
  • 複数のバリデーションを組み合わせて最終結果を生成
  • 階層的な derived ストアの構造で複雑なロジックを実現

コンポーネントでの使用例です。

javascript<script>
  import {
    email,
    password,
    passwordConfirm,
    emailValid,
    passwordValid,
    passwordConfirmValid,
    formValid
  } from './stores/form.js';

  function handleSubmit() {
    if ($formValid) {
      console.log('フォーム送信:', {
        email: $email,
        password: $password
      });
    }
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <div class="field">
    <label for="email">メールアドレス</label>
    <input
      id="email"
      type="email"
      bind:value={$email}
    />
    {#if $email && !$emailValid}
      <p class="error">有効なメールアドレスを入力してください</p>
    {/if}
  </div>

  <div class="field">
    <label for="password">パスワード</label>
    <input
      id="password"
      type="password"
      bind:value={$password}
    />
    {#if $password && !$passwordValid}
      <p class="error">パスワードは8文字以上で入力してください</p>
    {/if}
  </div>

  <div class="field">
    <label for="password-confirm">パスワード(確認)</label>
    <input
      id="password-confirm"
      type="password"
      bind:value={$passwordConfirm}
    />
    {#if $passwordConfirm && !$passwordConfirmValid}
      <p class="error">パスワードが一致しません</p>
    {/if}
  </div>

  <button type="submit" disabled={!$formValid}>
    登録
  </button>
</form>

<style>
  .error {
    color: red;
    font-size: 0.875rem;
    margin-top: 0.25rem;
  }
</style>

このコンポーネントでは、bind:value を使ってストアと入力フィールドを双方向バインディングしています。ユーザーが入力するたびに、対応する derived ストアが自動的に再計算され、エラーメッセージの表示や送信ボタンの有効/無効が切り替わります。

実践例 3:API データの管理

API から取得したデータをストアで管理する例を見てみましょう。

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

// ユーザーデータを管理するカスタムストア
function createUserStore() {
  const { subscribe, set, update } = writable({
    data: [],
    loading: false,
    error: null,
  });

  return {
    subscribe,

    // ユーザーデータを取得
    fetchUsers: async () => {
      // ローディング状態に設定
      update((state) => ({
        ...state,
        loading: true,
        error: null,
      }));

      try {
        const response = await fetch(
          'https://api.example.com/users'
        );

        if (!response.ok) {
          throw new Error(
            `HTTP error! status: ${response.status}`
          );
        }

        const data = await response.json();

        // データを設定
        update((state) => ({
          ...state,
          data: data,
          loading: false,
        }));
      } catch (error) {
        // エラーを設定
        update((state) => ({
          ...state,
          loading: false,
          error: error.message,
        }));
      }
    },

    // ストアをリセット
    reset: () =>
      set({
        data: [],
        loading: false,
        error: null,
      }),
  };
}

export const users = createUserStore();

このカスタムストアは、API データ、ローディング状態、エラー情報を一つのオブジェクトで管理しています。非同期処理の各段階で適切に状態を更新します。

derived ストアで特定のユーザーをフィルタリングすることもできます。

javascript// stores/users.js(続き)

// アクティブなユーザーのみをフィルタリング
export const activeUsers = derived(users, ($users) => {
  return $users.data.filter((user) => user.active === true);
});

// ユーザー数をカウント
export const userCount = derived(users, ($users) => {
  return $users.data.length;
});

コンポーネントでの使用例を見てみましょう。

javascript<script>
  import { onMount } from 'svelte';
  import { users, activeUsers, userCount } from './stores/users.js';

  // コンポーネント初期化時にデータを取得
  onMount(() => {
    users.fetchUsers();
  });
</script>

<div class="user-list">
  <h2>ユーザー一覧(全{$userCount}件)</h2>

  {#if $users.loading}
    <p>読み込み中...</p>
  {:else if $users.error}
    <p class="error">エラー: {$users.error}</p>
    <button on:click={() => users.fetchUsers()}>
      再試行
    </button>
  {:else}
    <h3>アクティブユーザー</h3>
    <ul>
      {#each $activeUsers as user (user.id)}
        <li>{user.name} - {user.email}</li>
      {/each}
    </ul>
  {/if}
</div>

このコンポーネントでは、ローディング状態、エラー状態、データ表示を適切に切り替えています。derived ストアを使うことで、アクティブなユーザーのみを簡単に表示できます。

まとめ

Svelte の「store is not a function」エラーは、ストアの仕組みを正しく理解することで確実に回避できます。

この記事で解説した主要なポイントを振り返りましょう。

#項目重要ポイント
1インポートストアオブジェクトをインポートし、$ 記法はコンポーネント内でのみ使用
2コンポーネント外get 関数または subscribe メソッドを使用
3derived ストア必ずストアオブジェクトを渡し、値を渡さない
4カスタムストアsubscribe メソッドを必ず公開する
5エラー対処エラーメッセージから原因を特定し、正しいパターンに修正

エラーが発生した際は、まずインポート方法と使用方法を確認しましょう。$ 記法の使用場所、derived に渡している引数、コンポーネント外でのアクセス方法など、チェックポイントは明確です。

Svelte のストアは、適切に使えば非常に強力な状態管理ツールになります。writable、readable、derived の特性を理解し、カスタムストアで独自のロジックを実装することで、保守性の高いアプリケーションを構築できるでしょう。

エラーを恐れず、この記事で紹介したパターンを参考に、Svelte ストアを活用してみてください。

関連リンク