Recoilを使ってみる。【React store管理】

以前ReactでLINEログインAPIの実装について説明したときに
状態管理としてReduxを使っていました。

https://qiita.com/raisack8/items/9336e815dacfa6a37a9e

その後状態管理について調べていたら
最近はReduxの上位互換の`Recoil`というのが
出ているらしいので、
そちらを使ったほうが世間で通用すると思い
ここにまとめておきます。

# 参考資料

https://www.youtube.com/watch?v=S93hsNFmIcM

Shinさん。
Reactの勉強をするのによく拝見しています。

# 簡単に特徴説明

ReduxはStoreを一元管理。
Recoilは複数のStoreを管理(単位: atom)

useContextはStoreが変わると
最上位コンポーネントまで変わり、再レンダリング。
Recoilは再レンダリングしない。

# 0.インストール
```sh
npm install recoil
```

# 1.適用範囲を決める: RecoilRoot

`RecoilRoot`をインポートし、
適用したい範囲をタグで囲む。
通常`index.tsx`の下記箇所を囲う。
```diff_typescript: index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
+ import { RecoilRoot } from "recoil";

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
+   <RecoilRoot>
      <App />
+   </RecoilRoot>
  </React.StrictMode>
);
```

# 2.storeを作成: atom
ファイルを作成
src>`stores`>`testStore.ts`

```typescript: testStore.ts
import { atom } from "recoil";

export const testStore = atom<string>({
    key: "testStore",
    // デフォルト値を指定する。
    default: "default value",
});
```
ここでは`testStore`という文字列型のstoreを作成

数字やオブジェクトリストを格納したい場合は型を変更
```typescript: 数字
import { atom } from "recoil";

export const testStore = atom<number>({
    key: "testStore",
    default: 0,
});
```

```typescript: オブジェクト配列
import { atom } from "recoil";

type OrgType = {
    id: string,
    title: string
}

// オブジェクト配列を作成する場合は型も定義しておく
export const testStore = atom<Array<OrgType>>({
    key: "testStore",
    default: [],
})
```
この後は文字列のほうで進める。

# 3.store値を取得/更新: useHooks

ファイルを作成
src>`Components`>`TestComponent.tsx`

## 値参照: useRecoilValue
storeを参照したいコンポーネントで
`useRecoilValue`を呼ぶ。

```diff_typescript: TestComponent.tsx
+ import { useRecoilValue } from 'recoil';
// 作成したstoreを読み込んでuseRecoilValueに渡す。
+ import { testStore } from '../stores/testStore';

const TestComponent = () => {
+   const testText = useRecoilValue(testStore);
    const handleClick = () => {
        alert(testText)
    }
    return (
        <div>
            <button onClick={()=>handleClick()}>click</button>
        </div>
    )
}
export default TestComponent
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/f1257082-acf8-29ad-f409-9f8eb09a4466.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/fce9684b-229f-4d56-fe30-30e2506cecc8.png)

動作確認完了。

## 値変更: useSetRecoilState;

storeを変更したいコンポーネントで
`useSetRecoilState`を呼ぶ。

```diff_typescript: TestComponent.tsx
// 'useSetRecoilState'を追加でimport
+ import { useRecoilValue, useSetRecoilState } from 'recoil';
import { testStore } from '../stores/testStore';

const TestComponent = () => {
    const testText = useRecoilValue(testStore);
+   const setTestText = useSetRecoilState(testStore)
    const handleClick = () => {
        alert(testText)
    }

    // setする関数を追加。
+   const handleClickSet = () => {
+       setTestText("setしました。")
+   }

    return (
        <div>
            <button onClick={()=>handleClick()}>click</button>
+           <button onClick={()=>handleClickSet()}>set</button>
        </div>
    )
}
export default TestComponent
```
`click`押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/9c1c2383-da78-f94c-0682-e3274be1c7d3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/ca9d5b70-5bbf-28c7-bfcf-901a94da35ce.png)
`set`押下→`click`押下

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/acd8c8e9-ef92-f038-b6f9-4f016e879cd5.png)

storeの値が変わっていることが確認できる

## 値取得/変更: useRecoilState;

storeを取得/変更したいコンポーネントで
`useRecoilState`を呼ぶ。

```diff_typescript: TestComponent.tsx
- import { useRecoilValue, useSetRecoilState } from 'recoil';
+ import { useRecoilState } from 'recoil';
import { testStore } from '../stores/testStore';

const TestComponent = () => {
-   const testText = useRecoilValue(testStore);
-   const setTestText = useSetRecoilState(testStore)
+   const [testText, setTestText] = useRecoilState(testStore)
    const handleClick = () => {
        alert(testText)
    }
    const handleClickSet = () => {
        setTestText("setしました。")
    }

    return (
        <div>
            <button onClick={()=>handleClick()}>click</button>
            <button onClick={()=>handleClickSet()}>set</button>
        </div>
    )
}
export default TestComponent
```
使い方は`useRecoilValue`, `useSetRecoilState`と同じ。

# 4.派生状態を取得: selector
storeの値を取得するだけなら
`useRecoilState`を使えば十分だが、
Recoilを学ぶにあたって`selector`
という概念が出てくる。

selectorは**加工したstore値**を返却する。
例
・フィルターされた todo リスト
・Todo リスト統計

https://recoiljs.org/docs/basic-tutorial/selectors

## selectorの作成方法
単純なstore値に文字列を付け加える処理
```diff_typescript: testStore.ts
+ import { atom, selector } from "recoil";

export const testStore = atom<string>({
    key: "testStore",
    default: "default value",
});

// 返り値の型を指定してあげる
+ export const testSelector = selector<string>({
+   key: "testSelector",
// get内でstore値に加えたい処理を記載する
+   get: ({ get })=>{
+       return `selected ${get(testStore)}`;
+   }
+})
```
## selectorの使用方法
```diff_typescript: TestComponent.tsx
+ import { useRecoilState, useRecoilValue } from 'recoil';
import { testSelector, testStore } from '../stores/testStore';

const TestComponent = () => {
    const [testText, setTestText] = useRecoilState(testStore)
+   const selectorValue = useRecoilValue(testSelector)
    const handleClick = () => {
        alert(testText)
    }
    const handleClickSet = () => {
        setTestText("setしました。")
    }
+   const handleClickSelector = () => {
+       alert(selectorValue)
+   }
    return (
        <div>
            <button onClick={()=>handleClick()}>click</button>
            <button onClick={()=>handleClickSet()}>set</button>
+           <button onClick={()=>handleClickSelector()}>selector</button>
        </div>
    )
}
export default TestComponent
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/aefbe9ff-1d8b-05ef-6a7c-530f4dabe908.png)

## 気になったので試してみたこと

`get(testStore)`しないと正しく文字列を取得できない。
最初`testStore`にそのまま文字列をつけようとしたが、
冷静に考えると`testStore`はobjectなので、
そのまま追加はできない。
```typescript
return `selected ${testStore}`;
```
こちらでalertしてみると一目瞭然。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/8fa79c8e-eb25-d8be-90a2-acb2a33e3c4c.png)

selectorに対してsetは使わないが、
試しに`useRecoilState`で呼び出してみることに。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1573902/6b57287d-0f37-8c27-11d2-5d506c65a951.png)


そりゃ普通の`testStore`と`testSelector`とで
宣言方法が違うのでエラーになる。

# おまけ: 「state」と「store」の違い

私自身この記事を書いていて
`state`と記述するか`store`と記述するか
迷っていたので、GPTに聞いてみた。


>Reactにおける「state」と「store」は、
>アプリケーションの状態管理に関連するが、
>役割と範囲が異なる概念です。
>
>State（ステート）
>
>定義:
>
>Stateは、Reactコンポーネント内の状態を表します。
>これは、コンポーネントが保持するデータであり、
>コンポーネントのレンダリングと行動に影響を与えます。
>
>範囲:
>
>Stateは通常、単一のコンポーネントに局所的です。
>つまり、あるコンポーネントのStateは、
>そのコンポーネント内でのみ管理され、
>必要に応じて子コンポーネントに
>プロパティ（props）として渡されます。
>
>
>Store（ストア）
>
>定義:
>
>Storeは、アプリケーションの全体的な状態を
>管理するための仕組みです。
>これは、状態管理ライブラリ（例：Redux、MobXなど）
>によって提供されます。
>
>範囲:
>
>Storeはアプリケーション全体にまたがる
>グローバルな状態を管理します。
>これにより、異なるコンポーネント間で状態を共有し、
>同期することが可能になります。