---
slug: "svelte5-runes-shared-store-library"
title: "Svelte5 の Runes Mode での共通ストアライブラリの書き方を考える"
description: "Svelte5 の Runes Mode で共通ストアのコードを書こうとすると、 $state が export できずに思い通りのコードが書けなかったため、妥協案を書きました。"
url: "https://www.ytyng.com/blog/svelte5-runes-shared-store-library"
publish_date: "2025-08-24T14:31:45Z"
created: "2025-08-24T14:31:45.821Z"
updated: "2026-02-27T10:47:00.343Z"
categories: ["Svelte"]
keywords: ""
featured_image_url: "https://media.ytyng.com/resize/20251018/f59cf8c7a0bf44ebb7f58ad54291f580.png.webp?width=768"
has_video: true
has_music: true
video_urls: ["https://media.ytyng.net/ytyng-blog/325/featured-video-1.mp4", "https://media.ytyng.net/ytyng-blog/325/featured-video-2.mp4", "https://media.ytyng.net/ytyng-blog/325/featured-video-3.mp4"]
music_urls: ["https://media.ytyng.net/ytyng-blog/325/featured-music-325-1.mp3", "https://media.ytyng.net/ytyng-blog/325/featured-music-325-2.mp3"]
lang: "ja"
---

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

## 結論

私は下記のような書き方をしている

### src/lib/stores/counter.svelte.ts
```ts
let counter = $state(0);

export default {
  get counter() {
    return counter;
  },
  increment: () => {
    counter += 1;
  }
};
```

## Svelte4 から Svelte5 への変化

Svelte5 では、状態管理の記法で Runes Mode が採用されています。

Svelte4 の時は、 .svelte ファイルでは

```ts
let counter = 0;
```

と書けば、暗黙的にリアクティブになりました。

また、共通ストアを使う場合、writable や get 等の関数を使い、

```ts
// 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 の記法に近づいたと言えます。

### この修正に対する意見
この変更は賛否両論の意見があります。

#### 批判的な意見

- [GitHub Discussion: Why this runes? It makes svelte look like React and I hate it](https://github.com/sveltejs/svelte/discussions/12455)
- [Loopwerk: First thoughts on Svelte 5's runes](https://www.loopwerk.io/articles/2025/svelte-5-runes/) - "not a big fan of the increased number of lines"
- [Medium: Svelte 5 runes— only dead fish follow the current](https://medium.com/@kimkorte/svelte-5-runes-only-dead-fish-follow-the-current-6e372a74918d)

####  肯定的な意見
- [Fine-Grained Reactivity in Svelte 5 – Frontend Masters](https://frontendmasters.com/blog/fine-grained-reactivity-in-svelte-5/)
- [Svelte 5 2025 Review: Runes and Other Exciting New Features](https://www.scalablepath.com/javascript/svelte-5-review)

## 大きな問題

特に大きな問題として、「`$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 を禁止し、より安全な方法での状態共有を強制しています。

しかし、この仕様のため、共通ストアを書こうとすると下記のようなコードになってしまいます。

[公式ドキュメント](https://svelte.jp/docs/svelte/$state)より

```ts
let count = $state(0);

export function getCount() {
    return count;
}

export function increment() {
    count += 1;
}
```

このコード、特に `getCount` の存在について、許せるでしょうか。私はかなりキツい印象です。Java男の加齢臭がします。

### その他の書き方

その他の書き方としては

#### 1. 関数でラップする方法（Svelte公式推奨）
```ts
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. クラスにする方法

```ts
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 にする方法

```ts
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 する方法

```ts
export const store = $state({
  counter: 0,
  increment() { this.counter += 1; },
  decrement() { this.counter -= 1; }
});
```

といった書き方ができるのですが、いずれにせよ大量のボイラープレートが必要で、従来の Svelte のコンパクトさは失われているように思います。

そもそも、共通ストアライブラリを書こうとした時に、「いろんな書き方がある」となってしまうのは良くないです。

せめて、公式ドキュメントのコードが納得感のあるものだったら良かったのですが。


## 結果

ボイラープレートをなるべく少なく書くと、以下のようになります。

### src/lib/stores/counter.svelte.ts
```ts
let counter = $state(0);

export default {
  get counter() {
    return counter;
  },
  increment: () => {
    console.log("increment");
    counter += 1;
  }
};
```

.svelte コンポーネントで使う場合は

```svelte
<script>
  import counter from '$lib/stores/counter.svelte.ts';
</script>

<p>Count: {counter.counter}</p>
<button onclick={() => counter.increment()}>
  Increment
</button>
```

となります。

### この書き方の理由


#### ボイラープレートが最小限
- 複雑な関数ラップや クラス定義が不要
- TypeScript の型定義も最小限で済む

#### 自然なカプセル化
- getter によって読み取り専用のインターフェースを提供
- メソッドによってのみ状態変更が可能
- 予期しない直接的な変更を防げる

#### 明示的なインターフェイスを定義する場合も悪く無い

```ts
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」が導入されており、オブジェクトの個別プロパティレベルでリアクティビティが追跡されます。つまり：

```ts
export const auth = $state({
  statusLoaded: false,
  statusLoading: false,
  authenticatedUser: null
});
```

このような場合、`auth.statusLoaded` が変更されても、`authenticatedUser` のみを使用しているコンポーネントは再描画されません。これは Svelte 5 の大きな性能向上の要因です。

## 最終的な感想

Svelte 5 の Runes は技術的には素晴らしい改善ですが、開発者体験の観点では賛否が分かれる変更だと感じます。特に共通ストアの書き方については、従来の Svelte の魅力だった「シンプルさ」が失われた感があります。
