0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Svelte] $derived の基本を理解したい

Last updated at Posted at 2025-11-07

$stateに引き続き、https://svelte.dev/docs/svelte/$derived を読んで理解を試みます。

$derived

state に依存する state を作成するための Rune。
state の更新に連動して derived の値が更新される。

<script>
	let count = $state(0);
    // count に連動して count * 2 の結果を返す state
	let doubled = $derived(count * 2);
</script>

<button onclick={() => count++}>
	{doubled}
</button>

<p>{count} doubled is {doubled}</p>

React などに親しんでいると以下のように書けると思うかもしれないが、これは動かない。

<script>
	let count = $state(0);
    // script ブロックは最初に一度実行されるだけで、
    // count が更新されても再実行されるわけではない
	let doubled = count * 2;
</script>

ちなみに $derived(count * 2) は以下のような JS コードに変換された。

let doubled = $.derived(() => $.get(count) * 2);

実際に起こることとしては、$derived, $derived.by から参照された state が、その derived state の依存先として登録される。
依存先 state が更新されるとその derived state は dirty になり、次回参照時に再計算される、ということらしい。
state を使いつつ依存先としないようにするには、untrack が使える。

$derived.by

複雑な derived state を作りたいときは、関数を引数にとれる $derived.by を使って構築する。

<script>
	let numbers = $state([1, 2, 3]);
	let total = $derived.by(() => {
		let total = 0;
		for (const n of numbers) {
			total += n;
		}
		return total;
	});
</script>

なお、$derived(count * 2)$derived.by(() => count * 2)) は全く同じ意味。

$derived の expression で別の state を更新してはいけない

$derived 使用時の注意として、

The expression inside $derived(...) should be free of side-effects. Svelte will disallow state changes (e.g. count++) inside derived expressions.

つまり $derived に記述する expression (上記例では count * 2) は副作用を持つべきではない (この中で他の変数を更新したりしてはいけない)。
なぜなら、更新をしてしまうと状態が不安定になるため (参考: https://svelte.dev/e/state_unsafe_mutation)。

以下の例では、表示される even, oddcount の値に対して不整合な状態になる可能性がある。

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

	let even = $state(true);

	let odd = $derived.by(() => {
        // ここで even を更新
		even = count % 2 === 0;

        //
        // この状態 (even, odd に不整合がある) で UI が更新されてしまうかもしれない!
        //
        
        // ここで odd を更新
		return !even;
	});
</script>

<button onclick={() => count++}>{count}</button>

<p>{count} is even: {even}</p>
<p>{count} is odd: {odd}</p>

こういう場合は以下のように両方 $derived にするとよい。

<script>
    let even = $derived(count % 2 === 0);
    let odd = $derived(!even);
</script>

なぜ1つ目の例で上手くいかないのか?(予想)

$derived.by の中で起こる state の更新は svelte コンパイル時にトラッキングされず、UI 更新同期の対象外になるため?

derived state は上書き可能

derived state が依存先の state の更新による更新以外にも、直接値を代入する事ができる (const でない限り)。

この性質を使って、楽観的 UI を実装することができる。
以下は、公式に乗っていたいいねボタンの例。

<script>
	let { post, like } = $props();

	let likes = $derived(post.likes);

	async function onclick() {
		// API のレスポンスを待たずに🧡を +1 しておく
		likes += 1;

		// ここで API を呼び出し、実際の🧡を +1 する
		try {
			await like();
            // 成功すると post が更新され post.likes が実際に +1 される
		} catch {
            // 失敗したのでロールバック
			likes -= 1;
		}
	}
</script>

<button {onclick}>🧡 {likes}</button>

derived state は deep reactive ではない

$state で作った state に含まれる Object や配列は Proxy オブジェクトで再帰的にラップされる。しかし、$derived はそうではない。

このコード例で、カラーピッカーの変更がそのまま上のパレットにも反映されている。
なぜこうなるかというと、上記の理由から selected は直接 items の要素 (つまり Proxy) を参照することになり、selected を変更することが、そのまま items を更新したことになるため。

<script>
    let items = $state([ /*...*/ ]);
    
    let index = $state(0);
    // selected は items[index] のオブジェクトそのものを参照している
    // (Proxy 化されていない)
    let selected = $derived(items[index]);
</script>

derived state は分割代入できる

<script>
    let { a, b, c } = $derived(stuff());
</script>

このように書くと、だいたい以下のような意味になる。

<script>
    let _stuff = $derived(stuff());
    let a = $derived(_stuff.a);
    let b = $derived(_stuff.b);
    let c = $derived(_stuff.c);
</script>

つまり、分割代入元のプロパティを依存先とする derived state が作られる。

derived state の値が変わらない場合の挙動

依存先 state を更新しても derived state が変わらない場合がある。
こういう場合、derived state で更新はストップし、それ以降に更新は伝播しない。

以下の例では、 largecount に依存し、さらに button のラベルが large に依存している。

<script>
	let count = $state(0);
	let large = $derived(count > 10);
</script>

<button onclick={() => count++}>
	{large}
</button>

この場合、count が 11 になるまで large の値は false のままであり、それまで button の更新は実行されない。
ここで重要なのはおそらく「ラベルの表示内容が変わらない」という話だけでなく、そもそも button 要素の更新処理自体が実行されない、ということだと思われる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?