【SolidJS】オブジェクトの状態にはcreateStore

# `createSignal` vs `createStore`
`createSignal`を使い、オブジェクトをシグナルとして保存するのは理想的でない場合がほとんどです。

言葉で説明するよりコードを見たほうが早いと思うのでさっそくいきましょう。

## `createSignal`の場合
```tsx
import { createSignal, createEffect } from "solid-js";

const App = () => {
  const initialUser = () => (
    { name: { first: "John", last: "Doe" },
      age: 69,
    }
  )
  const [user, setUser] = createSignal(initialUser());

  createEffect(() => {
    console.log("My age is", user().age);
  });

  createEffect(() => {
    console.log("My first name is", user().name.first);
  });

  const updateFirstName = () => {
    setUser((prev) => {
      const next = { ...prev };
      next.name.first = "Dan";
      return next;
    });
  };

  return (
    <>
      <div>
        Name: {user().name.first} {user().name.last}
      </div>
      <div>Age: {user().age}</div>
      <button onClick={updateFirstName}>Update Signal</button>
    </>
  );
};
```

:::note warn
`createSignal`に渡すオブジェクトはクローンされません。
```tsx
const initialUser = 
{ name: { first: "John", last: "Doe" },
  age: 69,
}

const [user, setUser] = createSignal(initialUser);
```
こう書いた場合、`initialUser`と`user`は同一のインスタンスです。

後で初期値を使いたい場合は、元の例のように関数として定義しましょう。
使わないなら、下のように`createStore`内に直接初期値を書くほうが好ましいです。
```tsx
const [user, setUser] = createSignal(
{ name: { first: "John", last: "Doe" },
  age: 69,
});
```

同じ初期値のシグナルを複数作成したいだけなら、そのためのプリミティブを作成するのがいいでしょう。
```tsx
const createUser = () => {
  return createSignal({
    name: { first: "John", last: "Doe" },
    age: 69,
  })
}

const [user, setUser] = createUser()
```

この特性は、後ほど紹介する`createStore`でも同様です。
:::

まず前提として、作成したシグナルは`signalObj`というオブジェクトそのものであり、そのプロパティではありません。
つまりSolidJSは、プロパティ一つの更新をオブジェクトそのものの更新と認識してしまいます。

この特性は`signalObj`に依存している全ての箇所の更新を引き起こします。

例ではクリック時に、`.name.first`のみを更新していますが、ボタンがクリックされる度に`signalObj()`と呼ばれている部分全てが再レンダーしてしまいます。
`.name.last`も`.age`も、値は変わっていないにも関わらずです。

`.age`にのみアクセスしているように見える副作用も、クリックの度に実行されてしまいます。

実際に試してみたい方はこちらの[playground](https://playground.solidjs.com/?version=1.4.1#NobwRAdghgtgpmAXGGUCWEwBowBcCeADgsrgM4Ae2YZA9gK4BOAxiWGjIbY7gAQi9GcCABM4jXgF9eAM0a0YvADo1aAGzQiAtACsyAegDucAEYqA3EogcuPfr2ZCouOAGU0Ac2hqsDpy4BRGRk4Zj5pOQVlVQ1tPQsrK2ZaCDI+AEFCQl4AXl4ACgBKXIA+fiteBxS03gw0XDQoNQBVMnFc8ohKyuh4RHsZNEY0-pUAKVoACwgVXzUoEeiAEVo4FSksCu6oDzh+gDYATk2uqUtT5NS+YHo2xl823FbxAF0OxzhnN09vfLqGprPRiFc5bD5fIIhML5IqlTrdKqpdRwAB0aloHnyKgAsvheDs4LUyLNeLdxEUUQSQVtJNSZhd-HBIaFcDDijkyiAtpVLnQ1Kj0ZicXjBsM+L1CWhidhSXcKRKUaK0nTKrTQRdqnx6IQRF8AGJDNIAOVghLysI58O6jyBMMIQgAbuzOdzurzxXAKHw8gIUX77XAHWdXT1PbgUQqld7llAZmBzgjKkJcEwuhAwwnumqaeqk3AU4wuljTpUADwlEO8UsiNAOislxMmvr8MmMeWmxWG3DSECt9vwNELbuV0v6Gt1kfjkrpXb9XtywqU3aSUdTkcmei4XApXgpADCGmYAGscr2dfqu024JISs1zy5eO4vE1Rxutyl6wjR5-eHTJOqhFEckLTKUtMmyfQSl8ERaGYeh4AgcNdlwAJ+QQ3AACF8AASRELEwCgLIVEKABCOkwEkF4gA)でどうぞ。インスペクターを使用すれば、`.name.last`や`.age`が再レンダーされている事が分かるはずです。

もちろんこんな動作は理想的ではありません。こちらの理想は:
- 更新されたプロパティに依存している箇所のみ更新される
- 更新されたプロパティに依存している副作用のみ実行される

というものです。

この理想を実現させてくれるのが`createStore`です。

## `createStore`の場合
[公式ドキュメント](https://www.solidjs.com/docs/latest/api#using-stores)

```tsx
//インポート元がcreateSignalとは違う点に注意
import { createStore } from "solid-js/store"; 
import { createEffect } from "solid-js";

const App = () => {
  const initialUser = () => (
    { name: { first: "John", last: "Doe" },
      age: 69,
    }
  )
  const [user, setUser] = createStore(initialUser());

  createEffect(() => {
    console.log("My age is", user.age);
  });

  createEffect(() => {
    console.log("My first name is", user.name.first);
  });

  const updateFirstName = () => {
    setUser("name", "first", "Dan");
  };

  return (
    <>
      <div>
        Name: {user.name.first} {user.name.last}
      </div>
      <div>Age: {user.age}</div>
      <button onClick={updateFirstName}>Update Signal</button>
    </>
  );
};
```
[playground](https://playground.solidjs.com/?version=1.4.1#NobwRAdghgtgpmAXGGUCWEwBowBcCeADgsrgM4Ae2YZA9gK4BOAxiWGjIbY7gAQi9GcCABM4jXgF9eAM0a0YvADo1aAGzQiAtACsyAegDucAEYqA3EogcuPfr2ZCouOAFEZMuMz7S5C5aoa2noWVjbcfAKOcM5wAMq43HBSsvKKKnRBugZkiUKhEFbMtBC5vACChIS8ALy8ABQAlLUAfPxWvA4lZRhouGhQagCqZOK17RCdndDwiPYyaIy5cyoAUrQAFoXYvGpQywEAIrRwKlJYHVNQAOZwcwBsAJwXk1KWr8WlfMD0o4xYvFGuBG4gAuuNorEEkl6r1+oMQYxGu9LpCXO5PN56k1WhMpl1Suo4AA6NS0a71FQAWXwvBuyTQZBUAN+4mJ9ORl0knMKHyc6I8Xlw2OaNTaIEunU+mRJZIp1NpCyWfBmDKZO1ZjGJquJStyPM63JRH26fHohBEsQAYotcgA5WDJOo4sV4qZAxGUyCO5kBPW4X0qQ5QbYGt5WS5CXBMSaU16dAA8LUlUwTIjQADdk-H8bwHbN+JrtY7dbbcNIQEWdXtcpIU4n9Oms-XeGnMy1yrc5pW-uzbpIE432y2EyZ6LhEpMSgBhDTMADWNUrFutZfzcEkLSGK5cvDiaGu0DUg7HE5K2fxg4vvB5kmNQlE4hFuITlWq+haAJEtGY9HgEFwYlblwVw1Dgf9cAAIXwABJEQvSgKoVEaABCHkwEkUEgA)
これで理想通りに動きます。が、`createSignal`とは構文が少し違うので一つづつ見ていきましょう。

### ゲッター関数がない
上の例で、`user`は関数ではなくオブジェクトです。
`createStore`によって作成されるオブジェクトは「プロキシ」と呼ばれる特殊な物で、裏ではそのプロパティ全てがシグナル化されています。（ネストされたプロパティも含む）
`user.age`にアクセスする時、裏ではシグナル化された`age`のゲッター関数にアクセスしている事になります。

ちなみに`props`もこのプロキシと呼ばれるオブジェクトです。
[こちらの記事](https://qiita.com/shadowTanaka/items/57764d62850bf14bc5f1)で`props`をうかつに分割代入するべきでない理由を解説していますが、`store`にも同じことが当てはまります。

### 特殊なセッター関数

```tsx
setUser("name", "first", "Dan");
```
とありますが、これは気持ち的に
```tsx
user.name.first = "Dan"
```
と書いている感じです。

他にも様々な使い方ができるので詳しくは[公式ドキュメント](https://www.solidjs.com/docs/latest/api#%E3%82%B9%E3%83%88%E3%82%A2%E3%81%AE%E6%9B%B4%E6%96%B0)を読んでみてください。

:::note
ver.1.4.0以前はトップレベルで配列を扱う事ができなかったので
```tsx
const [store, setStore] = createStore({arr: []})
```
のように、オブジェクトに包んで書く他ありませんでしたが、ver.1.4.0以降は
```tsx
const [store, setStore] = createStore([])
```
のように、トップレベルでの配列にも対応しています。

ただ、これは2022/6/11時点の日本語版ドキュメントには掲載されていないので注意してください。
:::

# オブジェクトや配列は全て`createStore`でいいのか

ほとんどの場合は`createStore`を使って問題ありません。

ですが、先程も述べた通り`createStore`から返されるプロキシオブジェクトは全てのプロパティをシグナル化してしまいます。
これによって更新される予定のないプロパティや値までウォッチされてしまい、パフォーマンスの低下に繋がる可能性もあります。


## 例1

次の例は、100万個の数字を配列に保存し、その平均値を表示するというものです。

```tsx
import { createStore } from "solid-js/store";

const produceBigArr = () => {
  return Array.from({ length: 1e6 }, () => Math.random());
};

const App = () => {
  const [arr, setArr] = createStore(produceBigArr());
  const updateArr = () => setArr(produceBigArr());
  const calculateAverage = () => {
    return (
      arr.reduce((sum, value) => {
        return sum + value;
      }, 0) / arr.length
    );
  };
  return (
    <>
      <button onClick={updateArr}>Update Array</button>
      <div>Average: {calculateAverage()}</div>
    </>
  );
};
```
`createStore`を用いると、100万個の値全てがシグナルとして登録されてしまうのでパフォーマンスがガタ落ちします。
[playground](https://playground.solidjs.com/?hash=-515973254&version=1.4.1)で実際に体験してみてください。（重いので注意）

[`createSignal`を使ったバージョン](https://playground.solidjs.com/?hash=1151047559&version=1.4.1)は軽々と動作します。

## 例2
こちらは少し極端な例ですが、「プロパティは沢山あるが、実際更新するのはほんの数個のみ」といったオブジェクトに関しても`createStore`だとパフォーマンスが低下する可能性があります。

```tsx
const [bigStore, setBigStore] = createStore(
  {
    foo: "foo",
    bar: false,
    baz: [1, 2, 3],
    // ...その他沢山のプロパティ
  }
)
```

例えばこの例の場合、`foo`のみが実際に更新される値だとすると、fooのみをシグナル化するほうがパフォーマンスは向上します。
```tsx
const [foo, setFoo] = createSignal("foo")
const bigObj = {
  foo,
  setFoo,
  bar: false,
  baz: [1, 2, 3],
  // ...その他沢山のプロパティ
}
```

クロージャにしてもいいですね。

```tsx
const bigObj = (() => {
  const [foo, setFoo] = createSignal("foo")
  return {
    foo,
    setFoo,
    bar: false,
    baz: [1, 2, 3],
    // ...その他沢山のプロパティ
  }
})()
```

# まとめ
今回は、`createStore`のメリットとデメリットを解説させて頂きました。

色々書きましたが、ぶっちゃけ`createStore`のデメリットが致命的になるケースは珍しいと思います。
値やプロパティの数がそれこそ数千単位になってやっと影響が出てくるようなものなので。

`createSignal`を用いて最小限のシグナルを作成する方法もあくまで「最適化」ですので、そこまで突き詰めなくてもいいケースも存在すると思います。

とりあえずオブジェクトや配列を状態として保存したくなったら`createStore`で大丈夫かなと。