Recoil を使って状態管理で遊んでみた

本日は、グローバルな状態を[Recoil](https://recoiljs.org/)で扱ったらどんな風に書けるのか、
ドキュメントを読みながら実際にサンプルコードを書いてみたのでシェアします。

実際に作ったサンプルのデモはこんな感じです。
![move.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184849/b1b54c61-4fbc-24b2-9acc-5bdd3cebd755.gif)


コードはこちらに載せています。
https://github.com/zhizit/recoil-practice

今回、「南極忘年会」の仕様としては以下のように考えました。

- 主催にかかる費用を設定できる
- 割り勘表にゲストを追加・削除ができる
- 追加された人数（匹？w）に応じて1人あたりの支払い金額を算出する
- 主賓は500円オフで、その分を主賓以外が割り勘する
- 1円単位だと小銭がバラバラするので細かいお金にならないよう100円単位で扱う

## セットアップ

まずはセットアップですね。Recoil をインストールしていきます。

```sh
# npm
$ npm install recoil
# yarn
$ yarn add recoil
```

こんな感じで[RecoilRoot](https://recoiljs.org/docs/introduction/getting-started#recoilroot)を実装します。

```tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RecoilRoot } from 'recoil'
import App from './App'

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

## AtomとSelector

Recoil には Atom と Selector という概念があります。

### Atom

Atom とは状態の単位のことを指していて、Atom が更新されると subscribe しているコンポーネントが再レンダリングする仕組みになっています。同じ Atom が複数のコンポーネントで使われるとき、すべてのコンポーネントがその状態をグローバルな状態として共有します。
今回のサンプルだと予算とゲストのリストを定義しています。

```tsx
export const arrangementPriceState = atom({
  // UniqueになるようにKeyを設定
  key: "ArrangementPrice",
  default: 13200,
});

export const personsState = atom({
  key: "Persons",
  default: [
    { id: 1000, name: "ペンギン", isMainGuest: false },
    { id: 1001, name: "トナカイ", isMainGuest: false },
    { id: 1002, name: "ベルーガ", isMainGuest: false },
    { id: 1003, name: "シロクマ", isMainGuest: true },
  ],
});
```

### Selector

Atom にもとづく派生 state で、Atom の値を何らかの計算や加工した結果を返します。

```tsx
import { selector } from "recoil";
// atomで定義したstate
import { personsState, arrangementPriceState } from "./atom";

// 主賓の人数を扱うStateとしてselectorを生成
export const mainGuestCountState = selector({
  // UniqueになるようにKeyを設定
  key: "MainGuestCount",
  get: ({ get }) => {
    // get(定義したState) でatomのアイテムを取得できる
    const persons = get(personsState);
    // 加工して返す
    return persons.filter((person) => person.isMainGuest).length;
  },
});

// 主賓ではないゲストの人数を扱うStateとしてselectorを生成
export const guestCountState = selector({
  key: "GuestCount",
  get: ({ get }) =>
    get(personsState).filter((person) => !person.isMainGuest).length,
});

// このように、使いたい用途に合わせて細かく定義が可能！
export const regularBillState = selector({
  key: "RegularBill",
  get: ({ get }) => {
    const arrangementPrice = get(arrangementPriceState);
    if (arrangementPrice < 1000) return 0;
    // 主賓は500円OFFなので、他のゲストが多めに払う
    const mainGuestCount = get(mainGuestCountState);
    const extraPrice = mainGuestCount * 500;
    const price =
      (arrangementPrice + extraPrice) / (get(guestCountState) + mainGuestCount);
    // 細かいお金にならないよう100円単位で請求
    return Math.ceil(price / 100) * 100;
  },
});

export const specialDiscountBillState = selector({
  key: "SpecialDiscountBill",
  get: ({ get }) => {
    const bill = get(regularBillState);
    return bill - 500 >= 0 ? bill - 500 : 0;
  },
});

// 以下のように複数のStateをまとめて書くこともできる
export const totalState = selector({
  key: "Total",
  get: ({ get }) => {
    const personCount = get(mainGuestCountState) + get(guestCountState);
    const actualAmount = 
    get(regularBillState) * get(guestCountState) +
    get(specialDiscountBillState) * get(mainGuestCountState);
    return {
      personCount,
      actualAmount
    }
  },
});
```


### 準備ができたら Atom や Selector から値を取得したり、更新してみましょう。

Recoil には様々な hook が用意されています。  
Atom: https://recoiljs.org/docs/api-reference/core/atom/  
Selector: https://recoiljs.org/docs/api-reference/core/selector/  

今回は、useRecoilState, useRecoilValue, useSetRecoilState を使いました。

#### useRecoilState

最初の要素が状態の値で、2 番目の要素が呼び出されたときに指定された状態の値を更新するセッター関数であるタプルを返します。
この hook は、コンポーネントを subscribe して、要求された状態の変更を再レンダリングします。

#### useRecoilValue

この hook は、Recoil の状態が変化した場合にコンポーネントを subscribe して再レンダリングします。

#### useSetRecoilState

この hook は、書き込み可能な Recoil 状態の値を更新するためのセッター関数を返します。
useRecoilState とは異なり、更新することに特化していて subscribe はしないため、Recoil の状態が変化しても再レンダリングされることはありません。


```tsx
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import { PersonalBill } from "./components/PersonalBill";
import { personsState, arrangementPriceState } from "./recoil/atom";
import { totalState } from "./recoil/selector";
import "./App.css";

export type Person = {
  id: number;
  name: string;
  isMainGuest: boolean;
};

function App() {
  // useRecoilState hook を使うと値の参照と登録ができる
  const [persons, setPersons] = useRecoilState(personsState);
  // useRecoilValue hook を使うと値を参照できる
  const { personCount, actualAmount } = useRecoilValue(totalState);
  // useSetRecoilState hook を使うと値を追加できる
  const setArrangementPrice = useSetRecoilState(arrangementPriceState);

  const handleAddPerson = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setPersons((oldList) => [
      ...oldList,
      {
        id: getId(),
        name: event.target.name.value,
        isMainGuest: event.target.isMainGuest.checked,
      },
    ]);
  };

  const onChangeArrangementPrice = (
    event: React.ChangeEvent<HTMLInputElement>
  ) => {
    setArrangementPrice(Number(event.target.value));
  };

  return (
    <div className="App">
      <h2>南極忘年会</h2>
      <h3>費用として集めたい金額の目安</h3>
      <div>
        <label>
          ¥：
          <input
            type="number"
            min="0"
            onChange={onChangeArrangementPrice}
            // WARNING: ちゃんと書くならAtomから取得するべき。useSetRecoilStateのsampleを書くためにこうしている。
            defaultValue="13200"
          />
        </label>
      </div>
      <h3>割り勘表</h3>
      <div>
        <p>※主賓は500円OFF</p>
        <p>※細かいお金にならないよう100円単位で請求</p>
        {persons.map((person) => (
          <PersonalBill key={person.id} person={person} />
        ))}
        <p>
          {personCount}人:合計¥
          {actualAmount.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")}
        </p>
      </div>
      <h3>割り勘表に追加</h3>
      <form onSubmit={handleAddPerson}>
        <label>
          <input type="checkbox" name="isMainGuest" />
          主賓
        </label>
        <label>
          <input type="text" name="name" placeholder="名前" required />
        </label>
        <input type="submit" value="決定" />
      </form>
    </div>
  );
}

export default App;

// utility for creating unique Id
let id = 1;
function getId() {
  return id++;
}
```

以上で、デモのような仕様を表現できました！
かなりシンプルですよね！


Flux や Redux は学習コストが高く、単一方向のデータフローを実現するのにコード量も多かったので、Recoil の手軽さにトキメキました！
他にも hook がたくさんあるので、またサンプルを書いてどんな動きになるのか、試して見ようと思います！