Svelte5 の Runes Mode での共通ストアライブラリの書き方を考える

Svelte
2025-08-24 23:31 (2日前) ytyng
View in English

結論

私は下記のような書き方をしている

src/lib/stores/counter.svelte.ts

let counter = $state(0);

export default {
  get counter() {
    return counter;
  },
  increment: () => {
    counter += 1;
  }
};

Svelte4 から Svelte5 への変化

Svelte5 では、状態管理の記法で Runes Mode が採用されています。

Svelte4 の時は、 .svelte ファイルでは

let counter = 0;

と書けば、暗黙的にリアクティブになりました。

また、共通ストアを使う場合、writable や get 等の関数を使い、

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

export const count = writable(0);

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

// Component.svelte
<script>
  import { count, increment } from './stores.js';
</script>

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

というコードを書いていました。

Svelte5 からは、このような書き方の他に Runes Mode という記法ができるようになっています。

これは、今までのように暗黙的にリアクティブになっていた変数を、$state() $derived() $effect() などの明示的な関数で包むことで、より明確で一貫したリアクティビティシステムを提供するものです。これにより、.svelte ファイル以外でも同じ記法でリアクティブな状態を作ることができるようになり、「ユニバーサルリアクティビティ」が実現されています。

この変更は、今までの Svelte っぽさから離れ、より React や Vue の記法に近づいたと言えます。

この修正に対する意見

この変更は賛否両論の意見があります。

批判的な意見

肯定的な意見

大きな問題

特に大きな問題として、「$state で作ったリアクティブ変数は、export できない」という仕様があります。

export しようとすると、下記のエラーでコンパイルが停止します。

[plugin:vite-plugin-svelte-module] src/lib/stores/auth.svelte.ts:3:0 Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties
https://svelte.dev/e/state_invalid_export

この仕様は、参照の安定性を保つという意図によるものです。$state で作られた変数を直接 export すると、その変数が再代入された際に、import している側の参照が切れてしまい、リアクティビティが失われる可能性があります。これを防ぐために、Svelte 5 では直接的な export を禁止し、より安全な方法での状態共有を強制しています。

しかし、この仕様のため、共通ストアを書こうとすると下記のようなコードになってしまいます。

公式ドキュメントより

let count = $state(0);

export function getCount() {
    return count;
}

export function increment() {
    count += 1;
}

このコード、特に getCount の存在について、許せるでしょうか。私はかなりキツい印象です。Java男の加齢臭がします。

その他の書き方

その他の書き方としては

1. 関数でラップする方法(Svelte公式推奨)

function createCounter() {
  let count = $state(0);

  return {
    get count() { return count; },
    set count(v) { count = v; },
    increment: () => count += 1,
    decrement: () => count -= 1
  };
}

export const counter = createCounter();

2. クラスにする方法

class CounterStore {
  #count = $state(0);

  get count() {
    return this.#count;
  }

  increment() {
    this.#count += 1;
  }

  decrement() {
    this.#count -= 1;
  }
}

export const counter = new CounterStore();

3. Vue のように .value を模した getter, setter にする方法

let _counter = $state(0);

export const counter = {
  get value() { return _counter; },
  set value(v) { _counter = v; }
};

export const increment = () => _counter += 1;
export const decrement = () => _counter -= 1;

4. オブジェクトのプロパティとして export する方法

export const store = $state({
  counter: 0,
  increment() { this.counter += 1; },
  decrement() { this.counter -= 1; }
});

といった書き方ができるのですが、いずれにせよ大量のボイラープレートが必要で、従来の Svelte のコンパクトさは失われているように思います。

そもそも、共通ストアライブラリを書こうとした時に、「いろんな書き方がある」となってしまうのは良くないです。

せめて、公式ドキュメントのコードが納得感のあるものだったら良かったのですが。

結果

ボイラープレートをなるべく少なく書くと、以下のようになります。

src/lib/stores/counter.svelte.ts

let counter = $state(0);

export default {
  get counter() {
    return counter;
  },
  increment: () => {
    console.log("increment");
    counter += 1;
  }
};

.svelte コンポーネントで使う場合は

<script>
  import counter from '$lib/stores/counter.svelte.ts';
</script>

<p>Count: {counter.counter}</p>
<button onclick={() => counter.increment()}>
  Increment
</button>

となります。

この書き方の理由

ボイラープレートが最小限

自然なカプセル化

明示的なインターフェイスを定義する場合も悪く無い

interface CounterStore {
  readonly counter: number;
  increment(): void;
  decrement(): void;
}

export default {
  get counter() { return counter; },
  increment: () => counter += 1,
  decrement: () => counter -= 1,
} satisfies CounterStore;

Fine-grained Reactivity の恩恵

なお、Svelte 5 では「Fine-grained Reactivity」が導入されており、オブジェクトの個別プロパティレベルでリアクティビティが追跡されます。つまり:

export const auth = $state({
  statusLoaded: false,
  statusLoading: false,
  authenticatedUser: null
});

このような場合、auth.statusLoaded が変更されても、authenticatedUser のみを使用しているコンポーネントは再描画されません。これは Svelte 5 の大きな性能向上の要因です。

最終的な感想

Svelte 5 の Runes は技術的には素晴らしい改善ですが、開発者体験の観点では賛否が分かれる変更だと感じます。特に共通ストアの書き方については、従来の Svelte の魅力だった「シンプルさ」が失われた感があります。

現在未評価
タイトルとURLをコピー
著者は、アプリケーション開発会社 Cyberneura を運営しています。
開発相談をお待ちしています。

アーカイブ

2025
2024
2023
2022
2021
2020
2019
2018
2017
2016
2015
2014
2013
2012
2011