Recoil最低限 CRA編（2021年8月）


Reactにおける最新のグローバルステート管理ライブラリである[Recoil](https://recoiljs.org/)を試してみたいと思います。**グローバルで利用できるuseState**と捉えるのが一番イメージに近いかもしれませんね。手軽に使えるのでマスターしておきたいところです。

>redux版も書きました。[こちらを](https://qiita.com/zaburo/private/fc3415ef771ffa6f6432)どうぞ。

## やりたいこと

* recoilのテスト
* グローバルで値が維持されていることを確認するために最低限のrouting（ページ移動）も行う
* 値の永続化も行う

やりたいことイメージをまとめると下記のような感じ。

![Kobito.lhGI5o.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/89adff93-eb14-fa63-b39d-78942bb79301.png "Kobito.lhGI5o.png")


## 準備

### 作業場の確保とライブラリのインストール

```bash
npx craete-react-app recoil-test
cd recoil-test

npm install recoil react-router-dom
```

## recoilと関係のない実装（骨組みづくり）

まず、複数ページからなる最低限の構造を作ります。とりあえずHomeとAboutからなるサイトにしてみます。

src以下にHome.jsとAbout.jsを作ります。

```bash
touch src/Home.js src/About
```

Home.jsを記述。

```js:Home.js
import { Link } from "react-router-dom";

const Home = () => {
    return (
        <>
            <h1>Home</h1>
            <Link to="/about">About</Link>
        </>
    );
}

export default Home;
```

>移動には\<a>\</a>ではなく\<Link>\</Lin>を利用してください。じゃないと、ページが更新されrecoilによる値の維持ができません。

About.jsを記述。

```js:About.js
import { Link } from "react-router-dom";

const About = () => {
    return (
        <>
            <h1>About</h1>
            <Link to="/">Home</Link>
        </>
    );
}

export default About;
```

作成したHome.jsとAbout.jsを読み込んでサイトの構造を作ります。

```js:App.js
import { BrowserRouter, Switch, Route } from "react-router-dom";

import Home from "./Home";
import About from "./About";

const App = () => {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/" exact component={Home} />
        <Route path="/about" exact component={About} />
        <Route render={() => <p>Page not found.</p>} />
      </Switch>
    </BrowserRouter>
  );
}
export default App;
```

一度、動作を確認して置きましょう。

```bash
npm start
```

ページ間を移動する簡単なサイトができました。
Recoilを利用して、このページ間で状態を維持するようにしてみたいと思います。

![スクリーンショット 2021-08-31 6.51.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/ef1fbe43-55ac-b9e4-87dd-1dc3786e82c5.png "スクリーンショット 2021-08-31 6.51.59.png")

## Recoilを利用した実装

ここからが本題です。

### RecoilRootで状態を共有したエリアを囲む

ReduxでもContextでも値を共有したいエリアを専用タグで囲みました。recoilではRecoilRootです。
多くの場合、サイト全体で値を共有するのでindex.jsに記述します。

```js:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { RecoilRoot } from "recoil"; //追加

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

reportWebVitals();
```

### Atomを作成

reacoilではatomと呼ばれる共有の変数を作成し値を共有します。
といらえずsrcの中にatoms.jsというファイルを作って、そこに記述します。

```bash
touch src/atoms.js
```

1つの変数につき１atom作る感じです。変数にはもちろんオブジェクトも持てます。
以下ではcountとuserの値を管理する2つのatomを作っています。

```js:atoms.js
import { atom } from "recoil";

export const countState = atom({
    key: "count",
    default: 0
});

export const userState = atom({
    key: "user",
    default: {
        name: "hoge",
        age: 11
    }
});
```

### Atomの利用（Home.jsでの表示と更新）

Home.jsを下記のようにしてみます。

```js:Home.js
import { Link } from "react-router-dom";
import { useRecoilState } from "recoil";
//atoms.jsを読み込み
import { countState, userState } from "./atoms";

const Home = () => {

    //useRecoilState（読み書きできる）
    const [count, setCount] = useRecoilState(countState);
    const [user, setUser] = useRecoilState(userState);

    //countをインクリメント
    const increment = (c) => {
        return c + 1;
    }

    //user.ageをインクリメント（機能的な意味はなし）
    const updateUser = (u) => {
        return { ...u, ...{ age: u.age + 1 } };
    }

    return (
        <>
            <h1>Home</h1>
            <p>count:{count}</p>
            <button onClick={() => setCount(increment)}>increment</button>
            <hr />
            <p>user.name:{user.name}, user.age:{user.age}</p>
            <button onClick={() => setUser(updateUser)}>user age increment</button><br/><br/>
            <Link to="/about">About</Link>
        </>
    );
}

export default Home;
```

実行すると下記のような感じになります。
ボタンを押すとcountやuser.ageがカウントアップされます。

![スクリーンショット 2021-08-31 7.23.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/9766f0a5-78bb-51c7-8d36-d5a4b2a85776.png "スクリーンショット 2021-08-31 7.23.08.png")

ゴールはこれらの値がAbout.jsでも利用できか？ということです。

### Atomの利用（About.jsでの表示：readonly）

About.jsを下記のようにします。
About.jsではuseRecoilStateではなくuseRecoilValueを利用してリードオンリーとしてみました。

>ここでは触れませんが、useSetRecoilState()というセットだけのhookもあり、それを利用すると再レンダリングを最小限に抑えることなどができます。

Homeで変更した値が反映されていればOKです。

```js:About.js
import { Link } from "react-router-dom";
import { useRecoilValue } from "recoil";
//atom.jsを読み込み
import { countState, userState } from "./atoms";

const About = () => {

    //useRecoilValue（読み取るだけ）
    const count = useRecoilValue(countState);
    const user = useRecoilValue(userState);

    return (
        <>
            <h1>About</h1>
            <p>count:{count}</p>
            <p>user_name:{user.name}</p>
            <p>user_age:{user.age}</p>
            <br/>
            <Link to="/">Home</Link>
        </>
    );
}

export default About;
```

変更した値がAbout.jsでも維持されていればOK。

![スクリーンショット 2021-08-31 7.28.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/207fe91f-b54d-5853-86b2-971eeb11070a.png "スクリーンショット 2021-08-31 7.28.24.png")

## Recoilの永続化

グローバル（ページ間）で値は共有できるようになりましたが、ブラウザを更新したら値は消えてしまします。
多くのシーンでは値の永続化が不可欠となります。Recoilには永続化の仕組みが備わっているようですが、少々煩雑なので[recoil-persistというモジュール](https://www.npmjs.com/package/recoil-persist)を利用してみたいと思います。

### モジュールのインストール

```bash
npm install recoil-persist
```

### atoms.jsに記述を追加

atoms.jsに数行追加するだけで永続化が可能となります。
ここでは2つのatomのうち、userStateだけ永続化の対象としてみます。

```js:atoms.js
import { atom } from "recoil";
import { recoilPersist } from "recoil-persist"; //追加

//標準でrecoil-persistというkey名でwebstorageに保存される（オプションで指定可能）
const { persistAtom } = recoilPersist();

export const countState = atom({
    key: "count",
    default: 0
});

export const userState = atom({
    key: "user",
    default: {
        name: "hoge",
        age: 11
    },
    effects_UNSTABLE: [persistAtom] //userStateにだけこの記述を追加
});
```

実行して動作確認してみます。
count, userどちらの値も変更してからブラウザを更新するとuserの方の値だけ保持されています。

![スクリーンショット 2021-08-31 7.47.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/4974cc0e-e72f-1f4f-8392-730ca09bd6a6.png "スクリーンショット 2021-08-31 7.47.02.png")

WebStorageを確認するとuserの値が保持されているのが確認できます（平文なので保存内容には注意を）。

## その他

他にselectorとかもありますが、ここまでで多くの用途に対応できるでしょう。
Reduxも悪くはないですが、recoilの簡単さを知るともう戻れないですね。