Reactステート管理ライブラリ Recoil入門（typescript）（UseContextの比較も）

## Recoilとは
Recoilは**React向けのステート管理ライブラリ**です。
Reactを開発したFacebook製のライブラリです。

https://recoiljs.org/docs/introduction/getting-started

Reactのステート管理ライブラリといったらReduxですが、
プロジェクト途中で導入する場合、非常にコストがかかります。

一方、RecoilはReact Hooksライクにコーディングが行えるため、学習コストも低く、
コード量も非常に軽量なので、導入が容易に行えるのが特徴です。

## やること
Recoilを用いて、メモ管理Webアプリを作ります。
また、類似ライブラリであるUseContextとの比較も行います。

## 前提条件
環境情報は以下になります。
#### 端末環境

| OS | チップ |
|:-----------:|:------------:|
| Mac Big Sur | Apple M1 |

#### 各種バージョン

```
% node -v
v14.17.0
% yarn -v
1.22.4
% npx create-react-app --version
4.0.3
```

## １. メモ管理Webアプリ作成
シンプルにメモの表示、追加、変更が行えるWebアプリになります。

### プロジェクト作成
`create-react-app`にてプロジェクト作成を行います。
今回はtypescriptで行います。

```
% npx create-react-app react-recoil-demo --template typescript
% cd react-recoil-demo
```

### Recoil インストール

```
% yarn add recoil
% yarn add @types/recoil
```

```
% npm install recoil
% npm install @types/recoil
```

### react-router-dom インストール

```
% yarn add react-router-dom
% yarn add @types/react-router-dom
```

```
% npm install react-router-dom
% npm install @types/react-router-dom
```

### tsconfig.json
``` tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src"
  ]
}
```


### RecoilRootの追加
`RecoilRoot`をインポートし、ルートコンポーネントを囲います。
ここではAppコンポーネントを囲います。

UseContextでは`Context.Provider`、
Reduxでは`Provider`で、Appコンポーネントを囲うのと同じですね。

``` index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { RecoilRoot } from 'recoil';

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

```

App.tsxはルートの定義をしています。

``` App.tsx
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";

import MemoList from "./pages/MemoList";
import MemoAdd from "./pages/MemoAdd";
import MemoEdit from "./pages/MemoEdit";

function App() {
  return (
    <Router>
      <Route exact path="/" component={MemoList} />
      <Route path="/add" component={MemoAdd} />
      <Route path="/edit/:memoId" component={MemoEdit} />
    </Router>
  );
}

export default App;

```

### atomの定義
Recoilは、**atom**というステートの定義ファイルを作成します。
ステートの型や初期値を定義しましょう。

``` atom.ts
import { atom } from "recoil";

export const memoAtomState = atom<string[]>({
  key: "memoAtom",
  default: [],
});

```

### atomの読み書き
atomは、コンポーネントから読み書きすることができます。

atomを読み取るコンポーネントは、暗黙的に**サブスクライブ**という状態になり
atomの値が変更されたら、対象のコンポーネントが再レンダリングされます。

よく使うコードの書き方はこちらです。

``` tsx
// 読み取りだけ
const memos = useRecoilValue<string[]>(memoAtomState);
// 書き込みだけ
const setMemos = useSetRecoilState<string[]>(memoAtomState);
// 読み込み書き込み両方
const [memos, setMemos] = useRecoilState<string[]>(memoAtomState);
```
*読み取り専用*、*書き込み専用*、*読み書き両方*の３種類の関数が用意されています。

1点注意が必要な点が、上記で述べた**atomを読み取るコンポーネントは、再レンダリングの対象となる**ことです。
つまり、atomに書き込みしかしないコンポーネントはサブスクライブされません。
そのため、書き込みしかしないコンポーネントは、`useSetRecoilState`を使うことで、
不要なレンダリングを減らすことができます。

以下、残りの一覧画面、追加画面、変更画面のソースとなります。

``` pages/MemoList.tsx
import React from "react";
import { useRecoilValue } from "recoil";
import { memoAtomState } from "../atom";

import { useHistory } from "react-router-dom";

const MemoList: React.FC = () => {
  const memos = useRecoilValue<string[]>(memoAtomState);

  const history = useHistory();
  const handleLink = (path: string) => history.push(path);

  return (
    <>
      {memos.map((memo, index) => (
        <li key={index}>
          <label>{memo}</label>
          <button onClick={() => handleLink("/edit/" + index)}>編集</button>
        </li>
      ))}
      <button onClick={() => handleLink("/add")}>追加</button>
    </>
  );
};

export default MemoList;
```

``` pages/MemoAdd.tsx
import React, { useRef } from "react";
import { useSetRecoilState } from "recoil";
import { memoAtomState } from "../atom";

import { useHistory } from "react-router-dom";

const MemoAdd: React.FC = () => {
  const history = useHistory();
  const textAreaReff = useRef<HTMLTextAreaElement>(null);
  const setMemos = useSetRecoilState<string[]>(memoAtomState);

  const handleClick = () => {
    if (textAreaReff.current !== null && textAreaReff.current!.value !== "") {
      setMemos((currVal: string[]) => {
        return [textAreaReff.current!.value, ...currVal];
      });
      history.push("/");
    }
  };

  return (
    <>
      <textarea ref={textAreaReff}></textarea>
      <button onClick={handleClick}>追加</button>
    </>
  );
};

export default MemoAdd;
```

``` pages/MemoEdit.tsx
import React, { useEffect, useRef } from "react";
import { useRecoilState } from "recoil";
import { memoAtomState } from "../atom";

import { useParams, useHistory } from "react-router-dom";

const MemoEdit: React.FC = () => {
  const history = useHistory();
  const textAreaReff = useRef<HTMLTextAreaElement>(null);
  const [memos, setMemos] = useRecoilState<string[]>(memoAtomState);
  const { memoId } = useParams<{ memoId?: string }>();

  const handleClick = () => {
    if (textAreaReff.current !== null && textAreaReff.current!.value !== "") {
      setMemos((currVal: string[]) => {
        return [
          textAreaReff.current!.value,
          ...currVal.filter((d, i) => i !== Number(memoId)),
        ];
      });
      history.push("/");
    }
  };

  useEffect(() => {
    textAreaReff.current!.value = memos[Number(memoId)];
  }, [memoId, memos]);

  return (
    <>
      <textarea ref={textAreaReff}></textarea>
      <button onClick={handleClick}>追加</button>
    </>
  );
};

export default MemoEdit;
```

### アプリ動作確認
デザインは置いておいて、このように動作します。
![MemoApp.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1457643/6db8aef9-a5e2-0f88-1104-3587de7db93e.gif)


## ２. Recoil vs UseContext 
Recoilの説明はここまでとなりますが、
ここで思うことが、**RecoilってUseContextにすごい似てる！！**という点です。

ただ、UseContextはラップしたコンポーネントを全てレンダリングするため、
管理するステートが増えるとレンダリングコストが肥大化してしまうというデメリットがあります。

この点、Recoilはどうなのかを調査しました。

### 調査内容
RecoilとUseContextを使って、同じ動作をするコードを作成します。
**動作時にレンダリングされるコンポーネントを観測します。**

アプリは、ボタンをクリックするとカウントアップするという簡単なものです。

### 観測１.UseContext
まずは、UseContextから観測を行います。
実行コードはこちら

``` App.tsx
import React, { createContext, useContext, useState } from "react";

const CountContext = createContext(
  {} as {
    count: number;
    setCount: React.Dispatch<React.SetStateAction<number>>;
  }
);

function App() {
  const [count, setCount] = useState(0);
  console.log("rendered App.");
  return (
    <CountContext.Provider value={{ count, setCount }}>
      <ShowCount />
      <IncrementButton />
      <ResetButton />
    </CountContext.Provider>
  );
}

const ShowCount = () => {
  const { count } = useContext(CountContext);
  console.log("rendered ShowCount.");
  return <div>{count}</div>;
};

const IncrementButton = () => {
  const { setCount } = useContext(CountContext);
  console.log("rendered IncrementButton.");
  return (
    <>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
    </>
  );
};

const ResetButton = () => {
  const { setCount } = useContext(CountContext);
  console.log("rendered ResetButton.");
  return (
    <>
      <button onClick={() => setCount(0)}>Reset</button>
    </>
  );
};

export default App;
```

### 結果１.UseContext
UseContextの実行結果がこちらになります。
ボタンをクリックする度に、**全てのコンポーネントがレンダリングされているのが分かります。**
本来であれば、ボタンのレンダリングは不要です。

![UseContext.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1457643/35b61477-426f-7853-4f19-dbdc2cb94f40.gif)

### 観測2.Recoil
続いて、Recoilの観測です。
実行コードはこちら

``` App.tsx
import React from "react";
import { useRecoilValue, useSetRecoilState } from "recoil";
import { countAtomState } from "./atom";

const App: React.FC = () => {
  console.log("rendered App.");

  return (
    <>
      <ShowCount />
      <IncrementButton />
      <ResetButton />
    </>
  );
};

const ShowCount: React.FC = () => {
  const count = useRecoilValue(countAtomState);
  console.log("rendered ShowCount.");
  return <div>{count}</div>;
};

const IncrementButton: React.FC = () => {
  const setCount = useSetRecoilState(countAtomState);

  const handleOnClick = () => {
    setCount((prevValue) => prevValue + 1);
  };

  console.log("rendered IncrementButton.");
  return <button onClick={handleOnClick}>+</button>;
};

const ResetButton: React.FC = () => {
  const setCount = useSetRecoilState(countAtomState);

  const handleOnClick = () => {
    setCount(0);
  };

  console.log("rendered ResetButton.");
  return <button onClick={handleOnClick}>Reset</button>;
};

export default App;
```

### 結果2.Recoil
Recoilの実行結果はこちらになります。
ボタンをクリックしても、**カウントを表示するコンポーネントしかレンダリングされません。**
さらに、注目したいのがResetボタンの挙動です。
Resetボタンの１回目クリックでは、Countステータスが変更されたので、レンダリングされますが、
２回目以降のクリックは、Countステータスは変わらないので、レンダリングされません。

このことから、Recoilのレンダリングは最小限で済むことが分かります。

![Recoil.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1457643/85152e3f-3faf-1188-7513-040695473c6f.gif)

### 結論
UseContextは、ステート変更時にラップされた全てのコンポーネントをレンダリングします。
一方、Recoilは、ステートを読み込んでいるコンポーネントのみレンダリングします。
このことから、**レンダリングコストはRecoilの方が低いことがわかりました。**

## 最後に
Reactのステート管理ライブラリRecoilの紹介とUseContextの比較した調査結果の共有をさせていただきました。
読んでいただいた方のお力になれば幸いです。

## 宣伝
パーソルプロセス＆テクノロジー株式会社（以下パーソルP&T）、システムソリューション（SSOL）事業部所属の堀江です。

私はモビリティソリューションデザインチームに所属しており、**モビリティ（ここでは移動手段全般）に関するサービスを考えたり、アプリを構築したりしております。**

いわゆる**「MaaS」**に取り組んでおります。

私たちが「MaaS」に取り組む中で、**現在活用している、もしくは活用する予定の技術やサービスやとりあえず発信したいこと**などなど、幅広くチームメンバーと共に執筆していきたいと思います。
メンバーごとに違った内容を発信していきますので、お楽しみに！

また、「MaaS」について詳しく知りたい方は、チームメンバーの吉田が記事を掲載しておりますので、
ぜひそちらをご覧ください。

[「MaaSとは」でたどり着いて欲しい記事　（1/3 前編）](https://note.com/ppt_msdg_maas/n/naae427dbc37c)
[「MaaSとは」でたどり着いて欲しい記事　（2/3 中編）](https://note.com/ppt_msdg_maas/n/na45c90b2f02f)
[「MaaSとは」でたどり着いて欲しい記事　（3/3 後編）](https://note.com/ppt_msdg_maas/n/n8fd724f7db6c)


**最後まで読んでいただき、ありがとうございました！**