🪸
Svelte5の$state,$bindable,$effectの挙動確認メモ
Svelte5のコンポーネントを作成している際に、クラスインスタンスを$props
の$bindable
で受けるとどのような挙動になるか確認したかったので、調べるついでにその他の基本的な型の操作も全てリアクティブという認識で合っているか確認したメモです。公式ドキュメントに書いていますが念のため。ついでに何となく感じていた$bindable
は$state
と同じでは疑惑や$effect
の$state
変数検知スコープ等も確認しました。確認用コードは簡単に作成したため愚直なコードです。当たり前ですが結果は公式ドキュメントに書いてある内容はそのままその通りでした。
($derived
の挙動メモはこちら)
確認結果
結論
-
$state
で宣言した変数は基本的に再割当・構造変更時に表示に反映される- オブジェクトであるかどうかには関わらない
- オブジェクトの構造内部を再割当・構造変更した際も反映される
- クラスインスタンスだけは
$state
で宣言しても変更検知が構造内部まで及ばない- フィールド値変更時に表示反映が必要な場合はフィールド定義に
$state
を含める - フィールド値が
$state
でない場合、即時ではなく他で変更検知された際に同時に表示反映される
- フィールド値変更時に表示反映が必要な場合はフィールド定義に
-
$bindable
は実質$state
で宣言した変数と同じ挙動になる-
$props
を受ける変数用の特別な$state
の書き方という認識で問題なさそう
-
-
$effect
はその中で使用している$state
変数の変更検知で発火する- 使用とは
stat;
のように単に$state
変数を書くだけで良い - クラスのフィールドも同様になる
-
$effect
内で関数がネストしていても関係ない- ネスト含む全ての関数内で使用されている
$state
変数の変更を検知する
- ネスト含む全ての関数内で使用されている
-
untrack
内で使用した$state
変数は変更検知の対象にならない- 変更検知の対象にならないだけで
untrack
内で検知対象が変更されると$effect
は発火する
- 変更検知の対象にならないだけで
- 使用とは
-
$effect
内で検知対象が変更された場合、無限ループに陥る- その場合は
$effect
の実行が全て終わった後、再度$effect
が呼び出される - 検知対象が変更された瞬間に次の
$effect
が呼ばれるわけではない
- その場合は
確認の前提
- 確認バージョン
svelte@5.0.0-next.241
(が最新リリースの状態でのREPL) - すべてのプリミティブ値はイミュータブル (MDN)
- この記事での表記
- 上記のため、代入操作は全て"再割当"と表記
-
number
,string
,boolean
,bigint
型を"無構造値"と表記 -
{}
を用いて自身で定義したオブジェクトを"オブジェクト"と表記 -
[]
を用いて自身で定義したオブジェクトを"配列"と表記 - 以下を"メンバー"と表記
- オブジェクトのプロパティ値
- 配列の要素値
- クラスインスタンスのフィールド値
- 以下の操作を"構造変更"と表記
- オブジェクトに対する場合はプロパティを増減させる操作
- 配列に対する場合は
.push()
等の要素数を増減させる操作
確認まとめ
- 確認主体:
let stat = $state(x)
のx
-
$state
,$bindable
共に同じ以下挙動
表示 反映 |
確認主体 | 確認内容 | コード例 |
---|---|---|---|
あり | 無構造値 | 再割当 | stat += 1; |
あり | オブジェクト | 再割当 | stat = {foo: 1}; |
あり | オブジェクト | 構造変更 | delete stat.foo; |
あり | オブジェクト | メンバーの再割当 (無構造値) | stat.foo += 1; |
あり | オブジェクト | メンバーの再割当 (オブジェクト) | stat.bar = {bar1: 1}; |
あり | オブジェクト | メンバーの再割当 (配列) | stat.baz = [0,0,0]; |
あり | オブジェクト | メンバーの構造変更 (オブジェクト) | delete stat.bar.bar1; |
あり | オブジェクト | メンバーの構造変更 (配列) | stat.baz.push(0); |
あり | オブジェクト | メンバーの無構造値メンバー再割当 (オブジェクト) | stat.bar.bar1 += 1; |
あり | オブジェクト | メンバーの無構造値メンバー再割当 (配列) | stat.baz[0] += 1; |
あり | 配列 | 再割当 | stat = [0,0,0]; |
あり | 配列 | 構造変更 (メンバーがオブジェクトの場合) | stat.push({foo: 1}); |
あり | 配列 | 構造変更 (それ以外の場合) | stat.push(0); |
あり | 配列 | メンバーの再割当 (無構造値) | stat[0] += 1; |
あり | 配列 | メンバーの再割当 (オブジェクト) | stat[0] = {foo: 1}; |
あり | 配列 | メンバーの再割当 (配列) | stat[0] = [0,0,0] |
あり | 配列 | メンバーの構造変更 (オブジェクト) | delete stat[0].foo; |
あり | 配列 | メンバーの構造変更 (配列) | stat[0].push(0); |
あり | 配列 | メンバーの無構造値メンバー再割当 (オブジェクト) | stat[0].foo += 1; |
あり | 配列 | メンバーの無構造値メンバー再割当 (配列) | stat[0][0] += 1; |
あり | クラス | 再割当 | stat = new Class1(); |
あり | クラス | $state有メンバーの再割当 (無構造値) | stat.foo += 1; |
なし | クラス | $state無メンバーの再割当 (無構造値) | stat.foo += 1; |
確認コード
$state
変数の描画確認
無構造値
code
App.svelte
<script>
let test1 = $state(0);
let test2 = $state("foo");
let test3 = $state(false);
let test4 = $state(0n);
const change1 = () => test1 += 1;
const change2 = () => test2 += "1";
const change3 = () => test3 = !test3;
const change4 = () => test4 += 1n;
function output() {
console.log(`${test1} ${test2} ${test3} ${test4}`);
}
</script>
<button type="button" onclick={change1}>test1</button>
<button type="button" onclick={change2}>test2</button>
<button type="button" onclick={change3}>test3</button>
<button type="button" onclick={change4}>test4</button>
<br>
<button type="button" onclick={output}>console.log</button>
<ul>
<li>{test1}</li>
<li>{test2}</li>
<li>{test3}</li>
<li>{test4}</li>
</ul>
オブジェクト
code
App.svelte
<script>
const init = {foo: 0, bar: {bar1: "bar1"}, baz: [1,2,3]};
let test = $state(init);
const changeFoo = () => test.foo += 1;
const changeBar1 = () => test.bar.bar1 += "1";
const changeBar2 = () => test.bar = {bar1: "bar"};
const changeBar3 = () => test.bar.bar2 = "hello";
const changeBar4 = () => delete test.bar.bar2;
const changeBaz1 = () => test.baz[0] += 1;
const changeBaz2 = () => test.baz.push(0);
const changeBaz3 = () => test.baz = [0,0,0];
const changeQux1 = () => test.qux = true;
const changeQux2 = () => delete test.qux;
const reset = () => test = init;
function output() {
console.log(`${test.foo} ${test.bar.bar1} ${test.baz} ${test.bar.bar2} ${test.qux}`);
}
</script>
<button type="button" onclick={changeFoo}>foo</button>
<br>
<button type="button" onclick={changeBar1}>bar1</button>
<button type="button" onclick={changeBar2}>bar2</button>
<button type="button" onclick={changeBar3}>bar3</button>
<button type="button" onclick={changeBar4}>bar4</button>
<br>
<button type="button" onclick={changeBaz1}>baz1</button>
<button type="button" onclick={changeBaz2}>baz2</button>
<button type="button" onclick={changeBaz3}>baz3</button>
<br>
<button type="button" onclick={changeQux1}>qux1</button>
<button type="button" onclick={changeQux2}>qux2</button>
<br>
<button type="button" onclick={reset}>reset</button>
<br>
<button type="button" onclick={output}>console.log</button>
<ul>
<li>{test.foo}</li>
<li>{test.bar.bar1} {test.bar.bar2}</li>
<li>
{#each test.baz as x}
{x};
{/each}
</li>
<li>{test.qux}</li>
</ul>
配列
code
App.svelte
<script>
const initFoo = [1,2,3];
const initBar = ["hello", false, 1, [1,2,3]];
let foo = $state(initFoo);
let bar = $state(initBar);
const changeFoo1 = () => foo[0] += 1;
const changeFoo2 = () => foo.pop();
const changeFoo3 = () => foo = [0,0];
const changeBar1 = () => bar[1] = !bar[1];
const changeBar2 = () => bar.push(0);
const changeBar3 = () => bar = ["yahoo",true,0,[0]];
const changeBar4 = () => bar[3][0] += 1;
const changeBar5 = () => bar[3].push(0);
const changeBar6 = () => bar[3] = [0,0,0];
const reset = () => {foo = initFoo; bar = initBar;}
function output() {
console.log(`${foo}`);
console.log(`${bar}`);
}
</script>
<button type="button" onclick={changeFoo1}>foo1</button>
<button type="button" onclick={changeFoo2}>foo2</button>
<button type="button" onclick={changeFoo3}>foo3</button>
<br>
<button type="button" onclick={changeBar1}>bar1</button>
<button type="button" onclick={changeBar2}>bar2</button>
<button type="button" onclick={changeBar3}>bar3</button>
<button type="button" onclick={changeBar4}>bar4</button>
<button type="button" onclick={changeBar5}>bar5</button>
<button type="button" onclick={changeBar6}>bar6</button>
<br>
<button type="button" onclick={reset}>reset</button>
<br>
<button type="button" onclick={output}>console.log</button>
<ul>
<li>
{#each foo as x}
{x};
{/each}
</li>
<li>
{#each bar as x}
{x};
{/each}
</li>
</ul>
オブジェクト配列
code
App.svelte
<script>
const init = {foo: 1, bar: {bar1: "bar1"}, baz: [1,2,3]};
let test = $state([init, init, init]);
const changeFoo = () => test[0].foo += 1;
const changeBar1 = () => test[0].bar.bar1 += "1";
const changeBar2 = () => test[0].bar = {bar1: "bar"};
const changeBar3 = () => test[0].bar.bar2 = "hello";
const changeBar4 = () => delete test[0].bar.bar2;
const changeBaz1 = () => test[0].baz[0] += 1;
const changeBaz2 = () => test[0].baz.push(0);
const changeBaz3 = () => test[0].baz = [0,0,0];
const changeQux1 = () => test[0].qux = true;
const changeQux2 = () => delete test[0].qux;
const changePush = () => test.push(init);
const changePop = () => test.pop(init);
const reset = () => test = [init, init, init];
function output() {
test.forEach(x => {
console.log(`${x.foo} ${x.bar.bar1} ${x.baz} ${x.bar.bar2} ${x.qux}`);
});
}
</script>
<button type="button" onclick={changeFoo}>foo</button>
<br>
<button type="button" onclick={changeBar1}>bar1</button>
<button type="button" onclick={changeBar2}>bar2</button>
<button type="button" onclick={changeBar3}>bar3</button>
<button type="button" onclick={changeBar4}>bar4</button>
<br>
<button type="button" onclick={changeBaz1}>baz1</button>
<button type="button" onclick={changeBaz2}>baz2</button>
<button type="button" onclick={changeBaz3}>baz3</button>
<br>
<button type="button" onclick={changeQux1}>qux1</button>
<button type="button" onclick={changeQux2}>qux2</button>
<br>
<button type="button" onclick={changePush}>push</button>
<button type="button" onclick={changePop}>pop</button>
<button type="button" onclick={reset}>reset</button>
<br>
<button type="button" onclick={output}>console.log</button>
<ul>
{#each test as x}
<li>{x.foo} {x.bar.bar1} {x.baz} {x.bar.bar2} {x.qux}</li>
{/each}
</ul>
クラス
code
App.svelte
<script module>
class Class1 {
foo = 0;
incrementFoo() { this.foo += 1; }
reset() { this.foo = 0; }
}
class Class2 {
foo = $state(0);
incrementFoo() { this.foo += 1; }
reset() { this.foo = 0; }
}
</script>
<script>
let cls1 = $state(new Class1());
let cls2 = new Class2();
let cls3 = $state(new Class2());
const foo1 = () => cls1.incrementFoo();
const foo2 = () => cls2.incrementFoo();
const foo3 = () => cls3.foo += 1;
const replace1 = () => cls1 = new Class1();
const replace2 = () => cls2 = new Class2();
const replace3 = () => cls3 = new Class2();
const reset = () => {
cls1.reset();
cls2.reset();
cls3.reset();
};
function output() {
console.log(`${cls1.foo} ${cls2.foo} ${cls3.foo}`);
}
</script>
<button type="button" onclick={foo1}>cls1.foo++</button>
<!-- <button type="button" onclick={cls2.incrementFoo}>not work</button> -->
<button type="button" onclick={foo2}>cls2.foo++</button>
<button type="button" onclick={foo3}>cls3.foo++</button>
<br>
<button type="button" onclick={replace1}>new cls1</button>
<button type="button" onclick={replace2}>new cls2</button>
<button type="button" onclick={replace3}>new cls3</button>
<br>
<button type="button" onclick={reset}>reset</button>
<br>
<button type="button" onclick={output}>console.log</button>
<ul>
<li>{cls1.foo}</li>
<li>{cls2.foo}</li>
<li>{cls3.foo}</li>
</ul>
$bindable
変数の描画確認
無構造値
code
App.svelte
<script>
import Comp from "./Comp.svelte"
</script>
<Comp />
Comp.svelte
<script>
let {
test1 = $bindable(0),
test2 = $bindable("foo"),
test3 = $bindable(false),
test4 = $bindable(0n),
} = $props();
const change1 = () => test1 += 1;
const change2 = () => test2 += "1";
const change3 = () => test3 = !test3;
const change4 = () => test4 += 1n;
function output() {
console.log(`${test1} ${test2} ${test3} ${test4}`);
}
</script>
<button type="button" onclick={change1}>test1</button>
<button type="button" onclick={change2}>test2</button>
<button type="button" onclick={change3}>test3</button>
<button type="button" onclick={change4}>test4</button>
<br>
<button type="button" onclick={output}>console.log</button>
<ul>
<li>{test1}</li>
<li>{test2}</li>
<li>{test3}</li>
<li>{test4}</li>
</ul>
オブジェクト
code
App.svelte
<script>
import Comp from "./Comp.svelte"
</script>
<Comp />
Comp.svelte
<script>
const init = {foo: 0, bar: {bar1: "bar1"}, baz: [1,2,3]};
let {
test = $bindable(init),
} = $props();
const changeFoo = () => test.foo += 1;
const changeBar1 = () => test.bar.bar1 += "1";
const changeBar2 = () => test.bar = {bar1: "bar"};
const changeBar3 = () => test.bar.bar2 = "hello";
const changeBar4 = () => delete test.bar.bar2;
const changeBaz1 = () => test.baz[0] += 1;
const changeBaz2 = () => test.baz.push(0);
const changeBaz3 = () => test.baz = [0,0,0];
const changeQux1 = () => test.qux = true;
const changeQux2 = () => delete test.qux;
const reset = () => test = init;
function output() {
console.log(`${test.foo} ${test.bar.bar1} ${test.baz} ${test.bar.bar2} ${test.qux}`);
}
</script>
<button type="button" onclick={changeFoo}>foo</button>
<br>
<button type="button" onclick={changeBar1}>bar1</button>
<button type="button" onclick={changeBar2}>bar2</button>
<button type="button" onclick={changeBar3}>bar3</button>
<button type="button" onclick={changeBar4}>bar4</button>
<br>
<button type="button" onclick={changeBaz1}>baz1</button>
<button type="button" onclick={changeBaz2}>baz2</button>
<button type="button" onclick={changeBaz3}>baz3</button>
<br>
<button type="button" onclick={changeQux1}>qux1</button>
<button type="button" onclick={changeQux2}>qux2</button>
<br>
<button type="button" onclick={reset}>reset</button>
<br>
<button type="button" onclick={output}>console.log</button>
<ul>
<li>{test.foo}</li>
<li>{test.bar.bar1} {test.bar.bar2}</li>
<li>
{#each test.baz as x}
{x};
{/each}
</li>
<li>{test.qux}</li>
</ul>
配列
code
App.svelte
<script>
import Comp from "./Comp.svelte"
</script>
<Comp />
Comp.svelte
<script>
const initFoo = [1,2,3];
const initBar = ["hello", false, 1, [1,2,3]];
let {
foo = $bindable(initFoo),
bar = $bindable(initBar),
} = $props();
const changeFoo1 = () => foo[0] += 1;
const changeFoo2 = () => foo.pop();
const changeFoo3 = () => foo = [0,0];
const changeBar1 = () => bar[1] = !bar[1];
const changeBar2 = () => bar.push(0);
const changeBar3 = () => bar = ["yahoo",true,0,[0]];
const changeBar4 = () => bar[3][0] += 1;
const changeBar5 = () => bar[3].push(0);
const changeBar6 = () => bar[3] = [0,0,0];
const reset = () => {foo = initFoo; bar = initBar;}
function output() {
console.log(`${foo}`);
console.log(`${bar}`);
}
</script>
<button type="button" onclick={changeFoo1}>foo1</button>
<button type="button" onclick={changeFoo2}>foo2</button>
<button type="button" onclick={changeFoo3}>foo3</button>
<br>
<button type="button" onclick={changeBar1}>bar1</button>
<button type="button" onclick={changeBar2}>bar2</button>
<button type="button" onclick={changeBar3}>bar3</button>
<button type="button" onclick={changeBar4}>bar4</button>
<button type="button" onclick={changeBar5}>bar5</button>
<button type="button" onclick={changeBar6}>bar6</button>
<br>
<button type="button" onclick={reset}>reset</button>
<br>
<button type="button" onclick={output}>console.log</button>
<ul>
<li>
{#each foo as x}
{x};
{/each}
</li>
<li>
{#each bar as x}
{x};
{/each}
</li>
</ul>
オブジェクト配列
code
App.svelte
<script>
import Comp from "./Comp.svelte"
</script>
<Comp />
Comp.svelte
<script>
const init = {foo: 1, bar: {bar1: "bar1"}, baz: [1,2,3]};
let {
test = $bindable([init, init, init]),
} = $props();
const changeFoo = () => test[0].foo += 1;
const changeBar1 = () => test[0].bar.bar1 += "1";
const changeBar2 = () => test[0].bar = {bar1: "bar"};
const changeBar3 = () => test[0].bar.bar2 = "hello";
const changeBar4 = () => delete test[0].bar.bar2;
const changeBaz1 = () => test[0].baz[0] += 1;
const changeBaz2 = () => test[0].baz.push(0);
const changeBaz3 = () => test[0].baz = [0,0,0];
const changeQux1 = () => test[0].qux = true;
const changeQux2 = () => delete test[0].qux;
const changePush = () => test.push(init);
const changePop = () => test.pop(init);
const reset = () => test = [init, init, init];
function output() {
test.forEach(x => {
console.log(`${x.foo} ${x.bar.bar1} ${x.baz} ${x.bar.bar2} ${x.qux}`);
});
}
</script>
<button type="button" onclick={changeFoo}>foo</button>
<br>
<button type="button" onclick={changeBar1}>bar1</button>
<button type="button" onclick={changeBar2}>bar2</button>
<button type="button" onclick={changeBar3}>bar3</button>
<button type="button" onclick={changeBar4}>bar4</button>
<br>
<button type="button" onclick={changeBaz1}>baz1</button>
<button type="button" onclick={changeBaz2}>baz2</button>
<button type="button" onclick={changeBaz3}>baz3</button>
<br>
<button type="button" onclick={changeQux1}>qux1</button>
<button type="button" onclick={changeQux2}>qux2</button>
<br>
<button type="button" onclick={changePush}>push</button>
<button type="button" onclick={changePop}>pop</button>
<button type="button" onclick={reset}>reset</button>
<br>
<button type="button" onclick={output}>console.log</button>
<ul>
{#each test as x}
<li>{x.foo} {x.bar.bar1} {x.baz} {x.bar.bar2} {x.qux}</li>
{/each}
</ul>
クラス
code
App.svelte
<script module>
class Class1 {
foo = 0;
incrementFoo() { this.foo += 1; }
reset() { this.foo = 0; }
}
class Class2 {
foo = $state(0);
incrementFoo() { this.foo += 1; }
reset() { this.foo = 0; }
}
</script>
<script>
let {
cls1 = $bindable(new Class1()),
cls2 = new Class2(),
cls3 = $bindable(new Class2()),
} = $props();
const foo1 = () => cls1.incrementFoo();
const foo2 = () => cls2.incrementFoo();
const foo3 = () => cls3.foo += 1;
const replace1 = () => cls1 = new Class1();
const replace2 = () => cls2 = new Class2();
const replace3 = () => cls3 = new Class2();
const reset = () => {
cls1.reset();
cls2.reset();
cls3.reset();
};
function output() {
console.log(`${cls1.foo} ${cls2.foo} ${cls3.foo}`);
}
</script>
<button type="button" onclick={foo1}>cls1.foo++</button>
<!-- <button type="button" onclick={cls2.incrementFoo}>not work</button> -->
<button type="button" onclick={foo2}>cls2.foo++</button>
<button type="button" onclick={foo3}>cls3.foo++</button>
<br>
<button type="button" onclick={replace1}>new cls1</button>
<button type="button" onclick={replace2}>new cls2</button>
<button type="button" onclick={replace3}>new cls3</button>
<br>
<button type="button" onclick={reset}>reset</button>
<br>
<button type="button" onclick={output}>console.log</button>
<ul>
<li>{cls1.foo}</li>
<li>{cls2.foo}</li>
<li>{cls3.foo}</li>
</ul>
$effect
挙動確認
基本形
code
App.svelte
<script>
let test1 = $state(0);
let test2 = 0;
$effect(() => {
console.log("run once");
});
$effect(() => {
test1;
test2;
console.log(`run at changeing test1 ${test1}`);
});
const increment1 = () => test1 += 1;
const increment2 = () => test2 += 1;
</script>
<button type="button" onclick={increment1}>test1</button>
<button type="button" onclick={increment2}>test2</button>
$effect
内で関数ネスト
code
App.svelte
<script>
let test1 = $state(0);
let test2 = $state(0);
let test3 = $state(0);
$effect(() => {
wrap1();
});
function wrap1() {
test1;
console.log(`run wrap1`);
wrap2();
}
function wrap2() {
test2;
console.log(`run wrap2`);
wrap3();
}
const wrap3 = () => {
test3;
console.log(`run wrap3`);
}
const increment1 = () => test1 += 1;
const increment2 = () => test2 += 1;
const increment3 = () => test3 += 1;
</script>
<button type="button" onclick={increment1}>test1</button>
<button type="button" onclick={increment2}>test2</button>
<button type="button" onclick={increment3}>test3</button>
untrack
の使用
code
App.svelte
<script>
import {untrack} from "svelte";
let test1 = $state(false);
let test2 = $state(0);
$effect(() => {
wrap1();
untrack(() => wrap2());
});
function wrap1() {
if (test1) {
untrack(() => {test1 = !test1;});
console.log(`run untrack`);
}
console.log(`run wrap1`);
}
function wrap2() {
test2;
console.log(`run wrap2`);
}
const increment1 = () => test1 = true;
const increment2 = () => test2 += 1;
</script>
<button type="button" onclick={increment1}>test1</button>
<button type="button" onclick={increment2}>test2</button>
$effect
再実行タイミング
クラスと
code
App.svelte
<script module>
class Class1 {
trigger = $state(false);
result = false;
}
</script>
<script>
let test = new Class1();
$effect(() => {
console.log(`start effect`);
if (test.trigger) {
console.log(`start wrap1`);
wrap1();
console.log(`end wrap1`);
}
console.log(`end effect`);
});
function wrap1() {
test.result = !test.result;
console.log(`change $state`);
test.trigger = false;
}
const trigger = () => test.trigger = true;
</script>
<button type="button" onclick={trigger}>trigger</button>
所感
生成されるスクリプトを見ると決して単純ではないのですが、使用する側から見た場合にはわかりやすめの素直な挙動になっている気がしました。
Discussion
めっちゃありがたすぎる記事です!
文字ばかり&そこまで面白味のない内容と思っていたのでそう言っていただけると嬉しいです。
この記事が誰かの役に立つ事があるなら他の小ネタぽいのも役立つか不明ですが出してみますね。