🪸

Svelte5の$state,$bindable,$effectの挙動確認メモ

2024/08/30に公開
2


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変数の描画確認

無構造値

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACpVSzW7CMAx-lcziUDS0jrYnSCvtOZZJdCVl0YJTNe6kqcq7T01ANIMD3Ozvx7a-ZIRWaWlh8z4C1kcJG3jrOlgB_XZTY3-kJgkrsGbomwnhtulVR5VAQVoSI2lpzUq2sFSTTF6X2zmTXRgBrTECYj6_8G2trYzZYjYXPYWCGoOWWPNV40FOi5MlK6vTGc8lW2__i7JIlE0iAWsBV8I8Ek7dky-uhEUkLPxa9Kp2wIaUQWYG6gZKlmyc4GA3Wr5oc0h2i9Gf61gosnORn4vC7UISTiBPL4kj_xyIDLLpeUoBoRPADDZaNd_leMrFVX4DT4OiutuZBWf2uDMPzvxxZxGcReTs7_GHlF01S3c2RCAftP-oXKvqlDlPtYqx7AaW38CKM8ZTPxZWcDR71Sq5hw31g3Qf7g-ePU4MUAMAAA==

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>

オブジェクト

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACp2US2-jMBSF_8qVpwuiQQmPrCggTRfdzzqOVEJMg8a1Cdgjxsj_fXRNmyeVWhYI-fp81z7g44FUNWcdSTYDEcUbIwn51TTEJ-pfg4PuL-OKEZ90UrclVtKubOtG5VRQVUrRKahFrSCDoZIygcCHXdEmMOyKNkyAEnxTYrFsEtiEfuTHW_uIOGcKFOsQfuhUoZiHrRY4d2peHgrxyp6lhAy8BWS5I5aVlPAzg_DxVvlUtOG1dFe0-ISopySkZIqJ7hh0dOGBEjvFxZNrYTdKDoxzOb3c-oTtGWeKXdMThLkzZTbB9rNPYO7smGWju4MXLKbUdyYMZLAJ_MAPtnf637q_2ctR95CBajWbEkeTVo-6vxC3rGPqqilk7lg5TaVFqWopQGrVaOUtYMDyCEvOlly-ei8Pw8fBsADvg49ff1kx9mY2OhWOurcv4xeyVKSr80kX6U4rJQVgLDJKxhElIEXJ6_JPNpzOqc0rKdPVqMiRbPMv83h6bY5bvurwVTZybDSLjR0bz2LXjl3Pd22cazPLtXGuzSzXxrk28dydYxhsftT9jJ1jNhwbfX91Fxibu9f36TFHNr_Iz0UTKlLN3fWe8jo_pypd8fqmfM7XTaKutGNYhx-sKA_nG6booLfjFFVDP96uVA0rlLmJiQUxoe_ldOV2SXzyJvd1VbM9SfAOslv7H5I7qMjVBgAA

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>

配列

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACp2U0Y6jIBSGX4Vl58JmSdXa9qJVk_ViXkJNBh0czTJgADczMbz7BnBaO-1spl4pnP87HA4_jLDpKJHwkI-Q4VcCD_B330ME1XtvBvIvoYpABCUfRG1mYlmLrldpwQpVcyYV6FinHjkHCchDtEFRebyMZViYWAFbQikvIAINppIgEKIPwiGUKNDYRA9SYUW8KfPqFK1sqlk0w8JGTwvWLWYv5JHzECTAW4EkNSnzoAS_EhAebwg3c-G65723uiWL5jKznwAF5ZUww-K8cIVFHpYgAT_c3y31Zq5e94NsveB6_QyLaC50_XzHLTf9VGIgKEB5UN5cYntRUFR-2YwMi90n7f8q2n_STk25bIsgkqiTcHS9m871OG1lOsijNlgzsFp1nAE-qH5Q3gqMZtrl45SsKX_xnh5MJv3k6rqKVVh8xHTBYv9sWRZXg1KcAePvpIBuVEDAWU27-k8yng2k04bzMPadJr2H3Vh2s4iNLBtdsOL7GYwDdVphsaBy40fLLqjcWNSy0SJ2a9ntInZn2d0idm_Z_f3dtsbWqf3cTztz63Rm3FmSgsUDtS9sTLvUWXz8SXDd2scHS_Cm3Wyhxjc9XYLRNwobiP2Ju-LNjfs2H_u2DIjgK3_umo48w4N5bHSp_wHIyIRPOQYAAA==

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>

オブジェクト配列

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACp2VQW-jMBCF_8rI2wPRogRIThSQdqXdc_aMkUqIKagUE7BXrJH_-2pMmyaFSA2HBOzM92Ze5AcDycuKdcSPB1Knr4z45EfTEJuIfw0uur-sEozYpOOyzXAn6LK2bEREayoyXncCyroUEMKQc-6Da8MhbX0YDmnr-kAJXinRuK18iF3bs7eJfkS8YgIE6xB-6EQqmBWjlg0f38kKK8-tsiKtn9lvziEEawVhZPjYSdY55_A9BPfxc_HPtHUn1Ye0xY-LCCUuJXOYN4eh0QtrlOg5dHurI2pSUrCq4vNNd2fyyCom2ERgBlJzBlXsJLf-ETVnTa0b2RWWs5oD5gwpCCF2bMd2kgnyR_bToU6yhxBEK9lcvXfL-Un2k_q97Ior_XF4PDLT-fe8-VTLm0lpyzomrsrQ3uQ8GiKXdSZKXgOXopHCWsGA21QY9Zy3v9KssHoUevth7MIrtq74s_X0MPR4YjUA3r2fxvNS6ct9b1ydZK-fxomp0OONpnWw-QhkHRykELwGTG9IybiiBHidVWX2Eg7nAOko5zzYjBURkm30ZR4zpSMc-Urhq6xnWG8RuzXsdhG7M-xuuWtlXKtFrpVxrRa5Vsa12i6dHNOoo5PsF0yOyTSst7Q7ZlVHmM_7u-95o6OGN_eQJsk6Mpf7Zx4jraOLvF6I0DqQlXn3Dd9YmhXjgyLtoNdjMIOqjM7hvs72e7Svkv0W7GBTlaPsBmVNrk0jYpNXfizzkh2Jj49Onej_Gzcpc7UHAAA=

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>

クラス

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACq1U227bMAz9FU7og4Pm0shviS1gGLCfmAY0c-RNqCIaFr1iMPTvg2THtZagTYu9yCZ1Dg9vds9qbZRju289s4eTYjv2uWnYktGfJhjutzKk2JI57NoqeApXtbohOOGxM0pIK6kyB-fgSzi30AePpBoRSnjYD5a2VatOytJXxGwBPdAv7dYBc1_Cdi_JD7hWOUUBIGlCxCgDwP-jxlO1O0cHUtnD4r-rFpuhaiFtMb1JMoqgMm77om3V89iIbDGkMWI4lDBd8iy5y6_w-cgPBaN1BDVi0MkWUIoouk7L2ydQPofyV6H5HJrP2jOBWtWYQ6VS-Xk92yTmCE9TuFZ-Ak_TeA3uFE3Ycf6xH-MY95OLX7ryxOXjWXe2Io0WsKOmGxZhwKN1aNTa4M_s8a6PIjWiB4gGnxuxcf5xjJtsjLTFj44ILYSPqpRssCQDtJXR1VPZh-F6cRa4vy82Aybs26fVCt4McDFlLywSPGP7NMWC1UrclguPufDLXG7g5pGbX-G2t0Q475oXYf6hI-_RP6_exOYfYOcTO_9I_k6RF_HxfvawgV7MNm8WRNqiM_HHUxgtXvax2BiduPl1dz53F5sYiy3ZCY-61urIdtR2yn_3fwGAeS7bFQYAAA==

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変数の描画確認

無構造値

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACpWTwW6DMAyGXyWzegANlRU4tQFp2mMsk0pp6KKFJAIzaUK8-5SkrNDu0N7s39_vOA4MUAvJO9i-D6DKhsMWXo2BCPDH2KT75hI5RNDpvq2sQruqFQYLphiKxugWyZtuDKlb3RAG69hma-9jwBSNLwZFHRoXTEEEjT6KWvAjbLHt-Rj9TWChe0eQHMlgA4bIO9yQnKwOQh3Lg-TBSxhdSsmixKDWmsEcSBdAXcqOz8vZsrXytdGqptWmC8KdvSLDSqsOSfVZqhO38wQhyQvip3vOyWZ3DSULKLEQgw2DGzBdgDZ7csENmC3AzB2rHFX3qkKhFdE9mh6DcNqetWvJ11Kfgv1qcOOOxAfJFKRTkI17d10cr1_40CNqRezj5Qx8xoBoVUlRfeXDeS9j4U6gsSeKu52JdyaPO1PvTB93Zt6ZLZztPX6_5bGYbXfWhCnaS_cZUymK885pLMVSS_7R0n-0bNJo7Nre_mMf4y-Qko5R8QMAAA==

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>

オブジェクト

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACp1VTY-bMBD9KyN3D4mKwkdyYgGprdR7z3GkJcRsUB2bgF1RI__3aszmm0pZDgg8fm9mHswzPSkrzloSr3si8gMjMflW18Qj6m-Ni_YP44oRj7RSNwVGkrZoqlplVFBVHWrZKPghDzWUjTwAJQsfV4uBRwkViX8hiMRB_YwK4pGD3FVlxXYkVo1m1jt3gKBnWyikaBVUolKQQl9KGUPgwTZvYui3eRPGQAneKbEYNjGsQy_ylhv7inTOFPT4QJViLeZ42VZil285m2HSuYebFuN1I-t2NkfauW6xz8U7-yklpDCbQ5oBZlmUUsLXFMLXe-T3vAlvodu8wStEPCUhJWOc6IGDYq_kUWLHeMvRWpiNkj3jXI6XW51pO8aZYrfsEYZ5EGXWweZ_r8A8yDGLWrf7WTAfQz-IMJDCOvACL9g84H_p7q6Xo-4gBZywMXA0KvWouytww1qmbpJC6ibOYUotClVJAVKrWqvZ_DRPSJacLbh8n7299KfBsAAfi9Onv44Ye7cbnQNH3dm34Q3Ze1tttVJSADompWRYUQJSFLwqfqf9eU5tVkqZ-AMiQ2aTPc3H6bUZtnyT4Vlu5LjRJO7ScZeTuCvHXU1XbZxqM0m1carNJNXGqTbLqZ2jGWx21N2EztEbjht9vrozjM3c7fPswUc2u_LPVRIqEs3dyZ_wKru4KvF5dRe--OvOUTfYwaz9F5YX-8sJk7fQ2WGLqr4bTleqeh9hbmOkIDr0I5z4rsvHv9zG_gP4a7VWcwcAAA==

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>

配列

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACp2V346jIBTGX4Vl56LNkqq17UWrJttN5iWKyaDFqVkKBHEzE8O7bwDbsX9203oleL7f4XD40A5WNaMNXO86yMmRwjX8KSVEUH9KO2n-UKYpRLARrSrtm6QpVS11hjnW9VEKpcEvcZSgUuIIMJwFdjbzHIaYJ8EXwBMnDTLMIYJHsa-rmu7hWquWGnSuwIoeLaEUvNGg5rV-FQKkYBehOYrzzWVsS5SNYXigjAkMEagIaygCEToRHmFUg84OsK5cvpei5ntSMDrp15giHy5cysvwligfNjYklZDNZGrznospD4S_01chIpCCyRSkGaiE2IU5-JGCaHNHOB8KZ1JIn_FaFg9ldq8hCvMb4Zaor4ULonZRDlLwzY_uqedD9Uy2zWES3q6_JSoeCn2vP8lB2F7b00Uh2oX53SUWFwXF-T-bsSVqeaX9X0WrK23flMu2KNpQfRZ2vnf9SW_6rfQnuzEWq1pe6lpwIFotWz2Znuxi8wlGZ0y8T95ebCbz5uu6iRVEnWLm-oIUrdaCA-v9FEM_wxAIXrK6_J12XwYyWSVElARekz3Dzh07H8XGjo0vWPV4ButAkxVEjajc-tGxIyq3FnVsPIpdOHYxil06djmKXTl29Xy3nbFN5h7P097cJhsYd5AE86Rl7uubsDrzFu--U1Ie3MeHNODD-LdYdx-mvwRdYBUukAQ9d8PbG_cwnwSujNtfSW7-AujOTfDYBgAA

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>

オブジェクト配列

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACp1V0W6bMBT9lSuvD0RDSSB9SgFpm7bn7jmOVEJMg0Z8HbAnBvK_T9e0aRKo1PJAsM05596D7gkdy4tS1Gy96ZhMj4Kt2TelmM_0P0Wb-q8otWA-q9FUGZ1EdVYVSidccl0cFVYafuBRQV7hETibL2g373mccRkt3ggyctBFwiXz2RH3RV6IPVvrygjrnzsg0EdbyFDWGgpZaIihyxHXEPiwS6s1dLu0CtbAGd05s3TcrmET-KG_2toHopdCQ0cLrrWoSeNuV8h9uiuFtyFVH95-tzOfoJZQqkJVezMSOXeRHVL5LH4hQgzeDOIESHOz3M5zRPgaQ_BwC_6eVsEAvUsrugKicBZwNkYLx2j0Di5cc2bHqKv3KpImZwdRljhe9P7M3ItSaDEQGCG1YwbbzXL73htpx6y1c2Xqg7ecjRHGDLUQw2bpL_3ldkD5bZphUyfTQAw0imP48D3nJ9MM8I-mPlzp983TDA37f0R1g0U1gFaiFvoKRvYGA-oYuZGZLlACGq2M9maXEz7PsfqZZgevIaGXB30VLMW8xGfv6a5raGItAK1ep_G8be3ledjvTqaxT33HXNt-YW_TvzNaowQKdsxZv-MMUGZlkf2Ju3OAbJIjRosekRCzSj7Mp0zZhFq-UvgoN3TccBJ35birSdx7x72f7rp1rttJrlvnup3kunWu29XUzimNNjmZZkLnlEzHDadWp6zahPL5-eqPqGyiUH2G6ZJsE3f7fM99pG1ykdcLES4jU7rPYvdFpNmh_6NIa2hsH8yoLJJzuK-z_Rrtq2S_BDtalEUvuyBZl2tXaPgV39r_iE2wLFMIAAA=

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>

クラス

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACq1V0Y6bMBD8la11D0SXwAXeEkCqTupP1JUuIaa1zngRXnqqkP-9skmISaJr7tQXol3PzI53FzKwWiph2Ob7wPSuEWzDvrYtWzL607rA_BaKBFsyg31XuUxuqk62VHLNSTYtdgTP2LRQd9gAZ3HionjkccZ1npwJOvfQpOSaLVmDB1lLcWAb6nphl5MDB7rDAjR46JXwTiq1Mwae3XMNg8twqhGhgKftGElddaIRmr4hRgsYgH5JEzvMYwHrLSc74jphBDkApwnhVUaAvaiWzqs9GNqRiJ4W_71q0MdwBErQyUGlzNpZ2Et92O2ViLR4O7YkWiyWEyiFAqajNApOstv09ES37rztsDWRv6BrBWpDUCO60tECihKcj3h-8e0MmobQ9F1oFkKzoHETqBOt2lViXj684XqmeYTPLVw05Bo-t_Ee3AiasMFc4uOAt-cpXKeyWcr6Z93riiRqwJ7aflyREY_aoBKxwp_Ry8Pgi9SIFsAHaRj4xtmXo-7lO7nviVCDe90KzsaIM0BdKVm9FoMbri1PBR4f82TEuE38slrBPwWupmxLjQRv2L1OWrBalfd5Sb2X9NrLHdzMc7Mb3O4ehdOu2dLN33XkI_VPqzex00-ws4mdfca_EWRL__Nx9riBtgw2LxDhOu-V_yTlSpbnfcwTJWfp9HY6C9N54rWu_yR-2L-MCys8sgYAAA==

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挙動確認

基本形

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACpVQS26EMAy9SmSxYNRRZ5olEKSeo6k0NGNoNMFBxFSqEHevQlpBP5uuHL-8j-0ZWuswQPE0AzU9QgGPwwBH4PchNuENHSMcIfhpNBGpghntwLUmzQ5ZMAZ-EEpkgRvG_Hwo9z9SKHGOCGnOsG3RcJ4fhKrFHCHNxlPwDu-d73IN40TCk0ENyWZJ5U_lGlxub1n-drxEw4aFeW2oQ0vd57jZvNblso-hpGVhyYzYI617pcwku1MiJf7gyW88-cWrTtutqHqZmD2JeFilIXUa4rrOmpuat9SlXuOqUyLV_xHLJJY7MRyh91fbWrxCweOEy_PyAdehTBP4AQAA

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内で関数ネスト

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACp2SYWuDMBCG_0o4-kGZrGvyzaqw3zEHuvQcoTERc24M8b8PTZ3a0cL2SZN73oe70x4qpdFB_NKDKWuEGJ6bBiKgr2Y8uA_UhBCBs10rx5vEyVY1lOUmJ43ECB0dWMp2jkrC4Ck8riv8ZkVcV0xOO6wqlBQEIUsz1o9XOX22ZXMIfHjwj6ozkpQ17FKb0amXo3-X1jir8VHb96BouwtbhMfFymfrLyffOvk9J984xdo58jRB47SbqaYV3NOKYhH9qJSRLdZopo17n9__Q8r84Fcc33D8Jic2nJi5ZL98bZO8dUTWsPHXSHPwpxyYNVIreU77pbshm9pK9h7K_hLmPsz_FRY-LFZhiKC2J1UpPEFMbYfD6_AN_yFUcPcCAAA=

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の使用

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACpVS22rEIBD9lansQ5Yu3a6Pmwv0O2phU1eLrBmDTlpK8N9LNGm2Fwp9EC_nMmfUkWljVWDHx5Fh2yl2ZA99z3aM3vtpE16VJcV2LLjBy-mkCtKbnhqBgkzXO08wDki-lZcI2rsOxKwSrJxI07CKgFSgA9SwCdSSKnRrg9qW1yhf0fuEoKCN0lpJKoot1A2MyZDefNsfiiwWNFefKRPGi20GY570gJKMQ5iFi4_RUKRYnyff3cYl9U1alHEpKkg6DM6qO-teipMfEGblaaHEPP3gpRAzK_6Ix9d46U7KP1z4lQtmDoFB6VWnMMXOXSxNkB9U-QuTf2FyuK3hUAqs9utbY_U8EDmE6WPUguWdYOBQWiMv9bjWjU0qWO0zqfmPmGcxvxKzHevc2Wijzuw4tRCf4gchqgevuAIAAA==

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再実行タイミング

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACm1S0W7DIAz8FQ_1gUpVq762SaRpnzEmNSMmQ6MmAmfVFOXfJ0KadlVfAmfuzofJIIx1GMXhfRBUn1EcxGvXiY3g3y6B-IOOUWxE9H3QqVJEHWzHcPZN77BSpFi7OkZ4S989DKmimINtWwxQwipyzShN7SKuj_k0YOwdQwlTdSqOiopd9q4UFctOsUMGxpjohJe5j5ysSPEKjUHNUq6hrK7dtafoHW6db-Upch0YMu10TWANyGS6nYOur9Kn4kuou_2iVTxhecP_JEjNg2B8kiqx_mUa82J60mw9wdxjGWgKu8zt5Q4en7jrr5panGe_5Li_8OPwp4f0FBluhDzTBxWHPonuHktR8dkze4L015RKZKQEeNLO6u9ymOVjNW-KXeZUisRGnH1jjcVGHJL5-DH-AQ1ws4yXAgAA

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

ryoppippiryoppippi

めっちゃありがたすぎる記事です!

scirexsscirexs

文字ばかり&そこまで面白味のない内容と思っていたのでそう言っていただけると嬉しいです。
この記事が誰かの役に立つ事があるなら他の小ネタぽいのも役立つか不明ですが出してみますね。