🍅
Svelte5の$derived挙動確認メモ
以前簡単に挙動確認していましたが本格的に使用するために$derived
と$derived.by
について色々挙動確認してみたところ、$effect
に頼らずとも色々できるかもしれない?と思ったのでその確認メモです。$state
変数の拡張のような使い方は1か月程度前は使えず萎えた気がするのですが、確認の仕方が悪かったのか、使えるようになった?かのようです。一部内容はSvelteの思想に沿わない使い方な気がするのでいつ動作しなくなるか不明です。
[svelte@5.0.0-next.243 (が最新リリースの状態でのREPLで確認)]
結論
-
$derived
変数に直接手動で再代入はできない-
$derived
変数がオブジェクト,配列の場合、要素再代入,構造変更は可能- クラスは検証していない
-
-
$derived
の中で使用した$state
変数の変更を検知すると更新される- 複数の
$state
変数が含まれているとそれら全てを検知する - 含まれる
$state
変数はオブジェクト,配列,クラスでも検知する- それらの 要素への個別代入,構造の変更,丸ごと再代入 いずれも検知する
-
$derived
の中で関数がネストしていても関係ない- ネスト含む全ての関数内で使用されている
$state
変数の変更を検知する
- ネスト含む全ての関数内で使用されている
-
untrack
内で使用した$state
変数は変更検知の対象にならない-
$effect
と同じような検知仕様の模様
-
- 複数の
-----ここから危険思想-----
-
$derived
内では他変数を変更できないが、$derived.by
内では変更できる-
$derived.by
内の再代入されるだけの$state
変数は変更検知の対象にならない -
$derived.by
内での変更は直感的な結果に反する場合がある- そのため
$derived.by
の実行タイミングを把握した上で実施する必要がある
- そのため
-
$derived.by
の実行タイミングは変更検知後$derived.by
変数にアクセスしたタイミング- 遅延評価のようなイメージ
-
$derived.by
変数にアクセスしなければ$derived.by
内で他変数を更新できない
-
確認内容とコード
再代入不可の確認
-
$derived
変数
コンパイルエラーになる。
-
$derived.by
変数
コンパイルエラーになる。
$state
変数の拡張
オブジェクト
code
App.svelte
<script>
let count1 = $state(0);
let count2 = $derived({foo: count1, bar: 0});
const output = () => console.log(`${count1}:${count2.foo}:${count2.bar}:${count2.baz}`);
function increment1() {
count1 += 1;
output();
}
function increment2() {
count2.bar += 1;
output();
}
function add() {
count2.baz = 1;
output();
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={increment2}>button2</button>
<button onclick={add}>add</button>
配列
code
App.svelte
<script>
let count1 = $state(0);
let count2 = $derived([count1, 0]);
const output = () => console.log(`${count1}:${count2[0]}:${count2[1]}:${count2[2]}`);
function increment1() {
count1 += 1;
output();
}
function increment2() {
count2[1] += 1;
output();
}
function add() {
count2.push(1);
output();
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={increment2}>button2</button>
<button onclick={add}>add</button>
トリガー確認
基本
code
App.svelte
<script>
let bool = true;
let count1 = $state(0);
let count2 = $derived(bool ? count1+1 : count1+2);
let count3 = $derived.by(() => {
if (bool) {
return count1 + 1;
} else {
return count1 + 2;
}
});
function increment1() {
count1 += 1;
console.log(`${count1}:${count2}:${count3}`);
}
function toggle() {
bool = !bool;
console.log(`${count1}:${count2}:${count3}`);
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={toggle}>toggle</button>
$state
複数の
code
App.svelte
<script>
let count1 = $state(0);
let count2 = $state(0);
let count3 = $derived(count1 + count2);
let count4 = $derived.by(() => {
return count1 + count2;
});
const output = () => console.log(`${count1}:${count2}; ${count3}:${count4}`);
function increment1() {
count1 += 1;
output();
}
function increment2() {
count2 += 1;
output();
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={increment2}>button2</button>
$state
反応するオブジェクト
- 基本
code
App.svelte
<script>
let count = $state({foo: 0, bar: 0});
let count3 = $derived(count.foo + count.bar);
let count4 = $derived.by(() => {
return count.foo + count.bar;
});
const output = () => console.log(`${count.foo}:${count.bar}; ${count3}:${count4}`);
function increment1() {
count.foo += 1;
output();
}
function increment2() {
count.bar += 1;
output();
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={increment2}>button2</button>
- 構造変更・再代入
code
App.svelte
<script>
let count = $state({foo: 0, bar: 0});
const count2 = {foo: 2, bar: 2};
let count3 = $derived(Object.values(count).reduce((acc, x) => acc+x, 0));
let count4 = $derived.by(() => {
return Object.values(count).reduce((acc, x) => acc+x, 0);
});
const output = () => console.log(`${count.foo}:${count.bar}:${count.baz}; ${count3}:${count4}`);
function add() {
count.baz = 1;
output();
}
function del() {
delete count.baz;
output();
}
function replace() {
count = count2;
output();
}
</script>
<button onclick={add}>add</button>
<button onclick={del}>del</button>
<button onclick={replace}>replace</button>
配列
- 基本
code
App.svelte
<script>
let count = $state([0, 0]);
let count3 = $derived(count[0] + count[1]);
let count4 = $derived.by(() => {
return count[0] + count[1];
});
const output = () => console.log(`${count[0]}:${count[1]}; ${count3}:${count4}`);
function increment1() {
count[0] += 1;
output();
}
function increment2() {
count[1] += 1;
output();
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={increment2}>button2</button>
- 構造変更・再代入
code
App.svelte
<script>
let count = $state([0, 0]);
const count2 = [2, 2];
let count3 = $derived(count.reduce((acc, x) => acc+x, 0));
let count4 = $derived.by(() => {
return count.reduce((acc, x) => acc+x, 0);
});
const output = () => console.log(`${count[0]}:${count[1]}:${count[2]}; ${count3}:${count4}`);
function add() {
count.push(1);
output();
}
function del() {
count.pop();
output();
}
function replace() {
count = count2;
output();
}
</script>
<button onclick={add}>add</button>
<button onclick={del}>del</button>
<button onclick={replace}>replace</button>
クラス
- 基本
code
App.svelte
<script module>
class Count {
foo = $state(0);
bar = 0;
}
</script>
<script>
let count = new Count();
let count3 = $derived(count.foo + count.bar);
let count4 = $derived.by(() => {
return count.foo + count.bar;
});
const output = () => console.log(`${count.foo}:${count.bar}; ${count3}:${count4}`);
function increment1() {
count.foo += 1;
output();
}
function increment2() {
count.bar += 1;
output();
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={increment2}>button2</button>
- 再代入
code
App.svelte
<script module>
class Count {
foo = $state(0);
bar = 0;
constructor(init) {
this.foo = init;
this.bar = init;
}
}
</script>
<script>
let count = $state(new Count(0));
const count2 = new Count(1);
let count3 = $derived(count.foo + count.bar);
let count4 = $derived.by(() => {
return count.foo + count.bar;
});
const output = () => console.log(`${count.foo}:${count.bar}; ${count3}:${count4}`);
function increment1() {
count.foo += 1;
output();
}
function increment2() {
count.bar += 1;
output();
}
function replace() {
count = count2;
output();
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={increment2}>button2</button>
<button onclick={replace}>replace</button>
検知スコープ
- 基本
code
App.svelte
<script>
let count1 = $state(0);
let count2 = $state(0);
let count3 = $derived(wrap1());
let count4 = $derived.by(() => {
return wrap1();
});
function wrap1() {
return count1 + wrap2();
}
function wrap2() {
return count2;
}
const output = () => console.log(`${count1}:${count2}; ${count3}:${count4}`);
function increment1() {
count1 += 1;
output();
}
function increment2() {
count2 += 1;
output();
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={increment2}>button2</button>
untrack
code
App.svelte
<script>
import { untrack } from "svelte";
let count1 = $state(0);
let count2 = $state(0);
let count3 = $derived(wrap1());
let count4 = $derived.by(() => {
return wrap1();
});
function wrap1() {
return count1 + untrack(()=>wrap2());
}
function wrap2() {
return count2;
}
const output = () => console.log(`${count1}:${count2}; ${count3}:${count4}`);
function increment1() {
count1 += 1;
output();
}
function increment2() {
count2 += 1;
output();
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={increment2}>button2</button>
$derived
内で他変数変更
$derived
- 複数式
コンパイルエラーになる。
code
App.svelte
<script>
let count1 = $state(0);
let count2 = 0;
let count3 = $derived(count2 = count1 + 1; count1 + 2); // compile error
function increment1() {
count1 += 1;
}
</script>
<button onclick={increment1}>button1</button>
- 単一式
この方法の場合、実行時エラーになる。
code
App.svelte
<script>
let count1 = $state([0]);
let count2 = 0;
let count3 = $derived(count1.map(x => {
count2 = count1 + 1;
count1 + 2;
}));
const output = () => console.log(`${count1[0]}:${count2}:${count3[0]}`);
function increment1() {
count1 += 1;
output();
}
</script>
<button onclick={increment1}>button1</button>
derived.by
- 通常の変数
更新されるが挙動が直感的でない印象。
code
App.svelte
<script>
let count1 = $state(0);
let count2 = 0;
let count3 = $derived.by(() => {
count2 = count1 + 1;
return count1 + 2;
});
const output = () => console.log(`${count1}:${count2}:${count3}`);
function increment1() {
count1 += 1;
output();
}
</script>
<button onclick={increment1}>button1</button>
-
$state
変数
更新されるが挙動が直感的でない印象。代入されるだけの$state
変数は変更検知されない模様。
code
App.svelte
<script>
let count1 = $state(0);
let count2 = $state(0);
let count3 = $derived.by(() => {
count2 = count1 + 1;
return count1 + 2;
});
const output = () => console.log(`${count1}:${count2}:${count3}`);
function increment1() {
count1 += 1;
output();
}
function increment2() {
count2 += 1;
output();
}
</script>
<button onclick={increment1}>button1</button>
<button onclick={increment2}>button2</button>
$derived.by
の実行タイミング
code
App.svelte
<script>
let count1 = $state(0);
let count2 = 0;
let count3 = $derived.by(() => {
console.log("derived fire");
count2 = count1 + 1;
console.log("derived end");
return count1 + 2;
});
function increment1() {
console.log("event fire");
console.log("state is changing");
count1 += 1;
console.log("state was changed");
console.log(`access to derived: ${count3}`); // toggle comment here
console.log(`${count1}:${count2}:${count3}`);
console.log("event end");
}
</script>
<button onclick={increment1}>button1</button>
コンポーネント構成例
- SelectPanel
code
App.svelte
<script>
import SelectPanel from "./SelectPanel.svelte";
let list = $state(["foo", "bar", "baz", "qux"]);
let result = $state([]);
function onclick() {
list.pop();
}
</script>
<button type="button" {onclick}>pop list</button>
<p>{result.length <= 0 ? "no selected" : result.join("; ")}</p>
<SelectPanel bind:list bind:result />
SelectPanel.svelte
<script>
import { untrack } from "svelte";
let { list = $bindable(), result = $bindable() } = $props();
let toggles = $derived(list.map(x => ({label: x, bool: false})));
const output = () => console.log(`${toggles[0]?.bool}:${toggles[1]?.bool}:${toggles[2]?.bool}:${toggles[3]?.bool}`);
function onclickF(index) {
return () => {
toggles[index].bool = !toggles[index].bool;
result = toggles.filter(x => x.bool).map(x => x.label);
output();
}
}
</script>
{#each toggles as toggle, i}
<button type="button" onclick={onclickF(i)}>{toggle.label}</button>
{/each}
所感と雑記
以前は$derived
変数はオブジェクトであってもfreeze
されているような状態だった気がします。そのために仕方なしに$effect
等で迂回していたのですが、迂回不要になったようです。$state.freeze
が廃止され$state.row
に変わった辺りで変わったのか...勘違いでなければですが。リリース前に使用しているので仕方ないですね。
リリースと言えば、5.0 Milestoneの残りOpen数が8 (98% complete)で、もうすぐSvelte5がリリースされそうなので楽しみです。
参考文献
- なし
Discussion