本文へスキップ

ルーンの紹介

リアクティビティの再考:再考

2019年、Svelte 3 は JavaScript をリアクティブな言語に変えました。Svelte は、コンパイラを使用して次のような宣言的なコンポーネントコードを…

アプリ
<script>
	let count = 0;

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

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

…状態(例:`count`)が変更されたときにドキュメントを更新する、きめ細かく最適化された JavaScript に変換するウェブ UI フレームワークです。コンパイラは `count` が参照されている場所を「認識」できるため、生成されるコードは非常に効率的であり、面倒な API を使用する代わりに `let` や `=` などの構文を乗っ取っているため、コードを少なく書くことができます。

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

Svelte 5 は、*普遍的で細粒度のリアクティビティ*を解き放つ *ルーン* でこれらをすべて変えます。

ルーンの紹介

始める前に

内部の動作を変更していますが、Svelte 5 はほとんどすべての人にとってドロップイン置換になるはずです。新しい機能はオプトインです。既存のコンポーネントは引き続き動作します。

Svelte 5 のリリース日はまだ決まっていません。ここで皆さんにお見せしているのは、変更される可能性のある開発中のものです!

ルーンとは何か?

ルーン /ro͞on/ 名詞

神秘的なまたは魔法のシンボルとして使用される文字または記号。

ルーンは、Svelte コンパイラに影響を与えるシンボルです。今日の Svelte は、特定の意味を持つ `let`、`=`、`export` キーワード、`$:` ラベルを使用していますが、ルーンは *関数構文* を使用して同じこと、そしてそれ以上のことを実現します。

たとえば、リアクティブな状態を宣言するには、`$state` ルーンを使用できます。

アプリ
<script>
	let count = 0;
	let count = $state(0);

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

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

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

いいえ、そうではありません。実際には、アプリケーションが複雑になるにつれて、どの値がリアクティブで、どれがそうでないかを判断することが難しくなります。そして、そのヒューリスティックはコンポーネントの最上位レベルでの `let` 宣言にしか機能せず、混乱を引き起こす可能性があります。`.svelte` ファイル内と `.js` ファイル内でコードが異なる動作をすることで、たとえば何かをストアに変換して複数の場所で使用する必要がある場合に、コードのリファクタリングが困難になる可能性があります。

コンポーネントを超えて

ルーンを使用すると、リアクティビティは `.svelte` ファイルの境界を超えて拡張されます。コンポーネント間で再利用できる方法でカウンターロジックをカプセル化したいとしましょう。現在では、`.js` または `.ts` ファイルでカスタムストアを使用します。

カウンター
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)
}; }

これは *ストア契約* (返された値に `subscribe` メソッドがある)を実装しているため、ストア名の前に `$` を付けることでストア値を参照できます。

アプリ
<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>

これは機能しますが、かなり奇妙です!より複雑なことを始めると、ストア API は扱いにくくなることがわかりました。

ルーンを使用すると、はるかにシンプルになります。

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.dokyumento.jp/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 }; }
アプリ
<script>
	import { createCounter } from './counter.svelte.js';

	const counter = createCounter();
</script>

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

`.svelte` コンポーネントの外部では、ルーンは `.svelte.js` と `.svelte.ts` モジュールでのみ使用できます。

返されたオブジェクトでget プロパティを使用していることに注意してください。これにより、`counter.count` は常に関数が呼び出された時点の値ではなく、現在の値を参照します。

ランタイムリアクティビティ

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

<script>
	export let width;
	export let height;

	// the compiler knows it should recalculate `area`
	// when either `width` or `height` change...
	$: area = width * height;

	// ...and that it should log the value of `area`
	// when _it_ changes
	$: 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` と `$effect` ルーンを導入しています。

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

	const area = $derived(width * height);

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

`$state` と同様に、`$derived` と `$effect` も `.js` と `.ts` ファイルで使用できます。

シグナルブースト

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

Svelte 5 のリアクティビティは、基本的に2010 年に Knockout が行っていたことである *シグナル* によって実現されています。最近では、シグナルは Solid によって普及し、他の多くのフレームワークによって採用されています。

しかし、私たちは少し異なる方法で物事をしています。Svelte 5 では、シグナルは直接やり取りするものではなく、内部実装の詳細です。そのため、同じ API デザインの制約がなくなり、効率性と人間工学の両方を最大化できます。たとえば、値が関数呼び出しによってアクセスされるときに発生する型ナローイングの問題を回避し、サーバーサイドレンダリングモードでコンパイルする場合、サーバー上ではオーバーヘッドにすぎないため、シグナルを完全に削除できます。

シグナルは *細粒度のリアクティビティ* を実現します。つまり、(たとえば)大きなリスト内の値の変更は、リストの他のすべてのメンバーを無効にする必要がありません。そのため、Svelte 5 は非常に高速です。

よりシンプルな時代へ

ルーンは追加機能ですが、既存の多くの概念を廃止します。

  • コンポーネントの最上位レベルとその他の場所での `let` の違い
  • export let
  • その付随する奇妙な動作を伴う `$:`
  • `<script>` と `<script context="module">` の動作の違い
  • `$$props` と `$$restProps`
  • ライフサイクル関数(`afterUpdate` などは単なる `$effect` 関数になります)
  • ストア API と `$` ストアプレフィックス(ストアはもう必要ありませんが、非推奨にはなっていません)

すでに Svelte を使用している皆さんにとって、これは新しいことを学ぶことになりますが、Svelte アプリの構築と保守を容易にするものです。しかし、新規ユーザーはそれらのすべてを学ぶ必要はありません。ドキュメントの「古いもの」というセクションに記載されているだけです。

しかし、これは始まりにすぎません。Svelte をよりシンプルでより強力にする、後続のリリースに関する多くのアイデアがあります。

試してみてください!

まだ本番環境で Svelte 5 を使用することはできません。現在、開発の真っ只中にあり、アプリで使用できる時期をお伝えすることはできません。

しかし、皆さんを待たせておくわけにはいきません。新しい機能の詳細な説明とインタラクティブなプレイグラウンドを備えたプレビューサイトを作成しました。Svelte Discordの `#svelte-5-runes` チャンネルにアクセスして、詳細を学ぶこともできます。皆様からのフィードバックをお待ちしております!