Skip to main content

Rune 導入 (Introducing runes)

‘rethinking reactivity’ を再考する

2019 年、Svelte 3 は JavaScript を リアクティブな言語 に変えました。Svelte はコンパイラを使用する web UI フレームワークで、このような宣言的なコンポーネントを…

App
<script>
	let count = 0;

	function increment() {
		count += 1;
	}
</script>

<button on:click={increment}>
	clicks: {count}
</button>

count のようなステートが変化したときに document を更新する、タイトに最適化された JavaScript に変換します。コンパイラは count が参照されている場所を ‘把握’ することができるので、生成されるコードは非常に効率的です。また、扱いにくい API を使用するのではなく、let= などの構文をジャックしているため、コード量を減らすことができます。

私たちがよく受けるフィードバックとして、’JavaScript をすべてこのように書けたらいいのに’ というものがあります。コンポーネント内のものが魔法のように更新されることに慣れていると、退屈で古い手続き型のコードに戻ることは、まるでカラーから白黒に戻るように感じられます。

Svelte 5 では、rune によって全てを変え、ユニバーサルできめ細やかなリアクティビティ(universal, fine-grained reactivity) を実現します。

Rune 導入

前書き

内部の動作を変更しているのですが、Svelte 5 はほとんどの方の場合ドロップインで置き換えられるはずです。この新機能はオプトインであるため、既存のコンポーネントを引き続き使用することができます。

Svelte 5 のリリース日はまだ決まっていません。ここでお見せするものはまだ作業中のものであり、変更される可能性があります!

Rune とは

rune /ro͞on/ noun

神秘的で魔法的なシンボルとして使用される文字、またはマーク。

Rune は Svelte コンパイラに働きかけるシンボルです。現在の Svelte では let=export キーワード、そして $: ラベル を特殊なものとして意味するように使用するのに対し、rune は同様のことを 関数の構文(function syntax) で実現します。

例えば、リアクティブなステートを宣言するには、$state という rune を使用します:

App
<script>
	let count = 0;
	let count = $state(0);

	function increment() {
		count += 1;
	}
</script>

<button on:click={increment}>
	clicks: {count}
</button>

一見すると、これは後退しているように見えるかもしれません。Svelte らしくない(un-Svelte-like)、と。デフォルトで let count がリアクティブであるほうが良いのではないか?と。

いいえ、それは違います。実際、アプリケーションが複雑になってくるにつれ、どの値がリアクティブでどの値がリアクティブでないのか判別するのが難しくなってくるのです。また、このヒューリスティックはコンポーネントのトップレベルにある let 宣言でのみ機能するため、混乱を招く可能性があります。コードの振る舞いが .svelte ファイル内と .js で異なっていると、コードのリファクタリングが難しくなります。例えば、何かを store にして複数の場所で使えるようにする必要がある場合などです。

コンポーネントを越えて

rune を使うと、リアクティビティが .svelte ファイルの境界を越えて拡がっていきます。counter のロジックをコンポーネント間で再利用できるようにカプセル化したいとします。現在は、custom store.js.ts ファイルで使用することになるでしょう:

counter
import { function writable<T>(value?: T | undefined, start?: StartStopNotifier<T> | undefined): Writable<T>

Create a Writable store that allows both updating and reading by subscription.

@paramvalue initial value
writable
} from 'svelte/store';
export function
function createCounter(): {
    subscribe: (this: void, run: Subscriber<number>, invalidate?: () => void) => Unsubscriber;
    increment: () => void;
}
createCounter
() {
const { const subscribe: (this: void, run: Subscriber<number>, invalidate?: () => void) => Unsubscriber

Subscribe on value changes.

subscribe
, const update: (this: void, updater: Updater<number>) => void

Update value using callback and inform subscribers.

update
} = writable<number>(value?: number | undefined, start?: StartStopNotifier<number> | undefined): Writable<number>

Create a Writable store that allows both updating and reading by subscription.

@paramvalue initial value
writable
(0);
return { subscribe: (this: void, run: Subscriber<number>, invalidate?: () => void) => Unsubscribersubscribe, increment: () => voidincrement: () => const update: (this: void, updater: Updater<number>) => void

Update value using callback and inform subscribers.

@paramupdater callback
update
((n: numbern) => n: numbern + 1)
}; }

これは ストアコントラクト(store contract) を実装しているため、戻り値には subscribe メソッドがあり、store の名前の先頭に $ を付けることで その store の値を参照することができます:

App
<script>
	import { createCounter } from './counter.js';

	const counter = createCounter();
	let count = 0;

	function increment() {
		count += 1;
	}
</script>

<button on:click={increment}>
	clicks: {count}
<button on:click={counter.increment}>
	clicks: {$counter}
</button>

動作しますが、かなり奇妙です! store API は、複雑なことをやり始めるとかなり扱いにくくなることがわかりました。

rune を使えば、もっとシンプルになります:

counter.svelte
import { writable } from 'svelte/store';

export function 
function createCounter(): {
    readonly count: number;
    increment: () => number;
}
createCounter
() {
const { subscribe, update } = writable(0); let let count: numbercount =
function $state<0>(initial: 0): 0 (+1 overload)
namespace $state

Declares reactive state.

Example:

let count = $state(0);

https://svelte.dev/docs/svelte/$state

@paraminitial The initial value
$state
(0);
return { subscribe, increment: () => update((n) => n + 1) get count: numbercount() { return let count: numbercount }, increment: () => numberincrement: () => let count: numbercount += 1 }; }
App
<script>
	import { createCounter } from './counter.svelte.js';

	const counter = createCounter();
</script>

<button on:click={counter.increment}>
	clicks: {$counter}
	clicks: {counter.count}
</button>

rune は、.svelte コンポーネント以外だと .svelte.js モジュールと .svelte.ts モジュールでのみ使うことができます。

戻り値のオブジェクトで get property を使用しているため、counter.count はこの関数が呼ばれたときの値ではなく、常に現在の値を指すことにご留意ください。

Runtime reactivity

現在、Svelte は コンパイル時のリアクティビティ(compile-time reactivity) を使用しています。これはつまり、依存しているものが変更されたときに自動的に再実行されるよう $: ラベルを使用しているコードがある場合において、それらの依存関係 (dependencies) は Svelte がコンポーネントをコンパイルするときに決定される、ということです:

<script>
	export let width;
	export let height;

	// コンパイラは、`width` や `height` のどちらかが変更されると
	// `area` を再計算する必要があることをわかっています…
	$: area = width * height;

	// …そして、`area` の値が変更されたときに
	// ログ出力することもわかっています…
	$: console.log(area);
</script>

これは上手く機能します…が、上手くいかないこともあります。上記のコードをリファクタリングしたとしましょう:

const const multiplyByHeight: (width: any) => numbermultiplyByHeight = (width) => width: anywidth * height;

$: area = const multiplyByHeight: (width: any) => numbermultiplyByHeight(width);

この $: area = ... の宣言では、width しか ‘把握’ することができないため、height が変更されても再計算されなくなります。結果として、コードをリファクタリングするのが難しくなり、複雑さがあるレベルを越えてくると Svelte がいつどの値を更新するのか、ということを理解するのが難しくなってきます。

Svelte 5 では $derived rune と $effect rune が導入され、評価されるときにその式の依存関係(dependencies)が決定されるようになります:

<script>
	let { width, height } = $props(); // instead of `export let`

	const area = $derived(width * height);

	$effect(() => {
		console.log(area);
	});
</script>

$state と同様、$derived$effect.js ファイルや .ts で使用することができます。

Signal boost

他のすべてのフレームワークと同じように、私たちは Knockout が正しかったことに気が付きました。

Svelte 5 のリアクティビティは signal によってもたらされており、これは本質的には Knockout が 2010年に行っていたことです。最近では、signal は Solid によって普及し、そして他の多くのフレームワークにも採用されています。

しかし、私たちの方法は少し異なります。Svelte 5 では、signal は直接操作をするものではなく、内部実装の詳細です。そのため、同様の API の設計上の制約がないため、効率とエルゴノミクスの両方を最大化することができます。例えば、関数呼び出しで値にアクセスすることによって発生する型の絞り込み(type narrowing)の問題を避けることができ、また、サーバーサイドレンダリングーモードでコンパイルする際には signal を完全に取り除くことができます (サーバー上では signal はオーバーヘッドだからです)。

Signal は きめ細やかなリアクティビティ(fine-grained reactivity) を実現し、それはつまり (例えば) 大きいリストの中にある値を変更しても、そのリストの他のメンバーを最新化(invalidate)する必要がないということです。そのため、Svelte 5 はとてつもなく高速です。

よりシンプルな時代の到来

Rune は付加的な機能ですが、既存のコンセプトの多くを不要なものにします:

  • コンポーネントのトップレベルにある let とそれ以外の場所にある let の違い
  • export let
  • $: と、これに付随する風変わりなもの全て
  • <script><script context="module"> の振る舞いの違い
  • $$props$$restProps
  • ライフサイクル関数 (onMount などは $effect 関数で良いでしょう)
  • store API と $ 接頭辞 (store は不要になりますが、非推奨ではありません)

すでに Svelte を使用している方々にとっては、新たに学ぶことがありますが、これが Svelte アプリの構築とメンテナンスをより簡単になればと思っています。これから Svelte を使う方は、全てを学習する必要はありません。ドキュメントに ‘old stuff’ というタイトルのセクションがあるだけになるでしょう。

これはまだ始まりに過ぎません。今後のリリースに向けて、Svelte をよりシンプルで高機能にするアイデアでいっぱいの長いリストがあります。

Try it!

まだ Svelte 5 をプロダクションで使用することはできません。私たちは今現在作業中で、いつあなたのアプリで使えるようになるのかお伝えすることができません。

しかし、私たちはみなさんを待たせたままにしたくありませんでした。プレビューサイトを作成し、新機能の詳細な説明と、インタラクティブな playground を用意しました。また、Svelte Discord#svelte-5-runes チャンネルでより詳しく学ぶこともできます。みなさんのフィードバックをお待ちしております!