私は下記のような書き方をしている
let counter = $state(0);
export default {
get counter() {
return counter;
},
increment: () => {
counter += 1;
}
};
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男の加齢臭がします。
その他の書き方としては
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();
class CounterStore {
#count = $state(0);
get count() {
return this.#count;
}
increment() {
this.#count += 1;
}
decrement() {
this.#count -= 1;
}
}
export const counter = new CounterStore();
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;
export const store = $state({
counter: 0,
increment() { this.counter += 1; },
decrement() { this.counter -= 1; }
});
といった書き方ができるのですが、いずれにせよ大量のボイラープレートが必要で、従来の Svelte のコンパクトさは失われているように思います。
そもそも、共通ストアライブラリを書こうとした時に、「いろんな書き方がある」となってしまうのは良くないです。
せめて、公式ドキュメントのコードが納得感のあるものだったら良かったのですが。
ボイラープレートをなるべく少なく書くと、以下のようになります。
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;
なお、Svelte 5 では「Fine-grained Reactivity」が導入されており、オブジェクトの個別プロパティレベルでリアクティビティが追跡されます。つまり:
export const auth = $state({
statusLoaded: false,
statusLoading: false,
authenticatedUser: null
});
このような場合、auth.statusLoaded
が変更されても、authenticatedUser
のみを使用しているコンポーネントは再描画されません。これは Svelte 5 の大きな性能向上の要因です。
Svelte 5 の Runes は技術的には素晴らしい改善ですが、開発者体験の観点では賛否が分かれる変更だと感じます。特に共通ストアの書き方については、従来の Svelte の魅力だった「シンプルさ」が失われた感があります。