Recoilに入門してみる

なにかと話題になる[Recoil](https://recoiljs.org/)ですが、未だ触ったことがなかったので公式ドキュメントを読みつつ、入門してみたいと思います。

# 本記事の目的

- [Recoilの公式ドキュメント](https://recoiljs.org/docs/introduction/installation)の`Introduction`と`Basic Tutorial`を参考に、Atom、Selectorの基本を学ぶ

# 本記事で扱わないこと

- Recoilの応用
- Recoilの運用や設計
- 他ライブラリとの比較

# Recoilとは

RecoilはReactの状態管理ライブラリです。Reactにおいてグローバルな状態をどう管理するかというのは悩ましい話題の一つです。ビルドインで備わっている状態管理機構（Context APIやpropsのバケツリレー）もありますが、それはそれでいくつかの制約があります（詳しくはドキュメントの[Motivation](https://recoiljs.org/docs/introduction/motivation)をご参照ください）。Recoilは、そのような問題を改善するために開発されているという背景があるようです。

# Recoilに入門する

Recoilの重要な概念としてAtomとSelectorが存在します。本記事ではこのAtomとSelectorの基本について学んでいきます。

## RecoilRoot

まず最初の準備としてRecoilを使用するためには、どこかの祖先・親コンポーネントで`RecoilRoot`を使用する必要があります。

```tsx
import { RecoilRoot } from 'recoil';
import { CharacterCounter } from './components/CharacterCounter';

const App = () => {
  return (
    <RecoilRoot>
     　{/* この中でRecoilが使える */}
      <CharacterCounter />
    </RecoilRoot>
  );
};

export default App;
```

## Atom

Atomは状態の一部を表します。Atomは任意のコンポーネントから状態の読み書きをすることができます。あるコンポーネントからAtomの状態を書き換えると、そのAtomを読み込んでいるすべてのコンポーネントが再レンダリングされ、書き込んだ状態が反映されます。いわゆるグローバルな状態をAtomに保持することが可能です。

```tsx
import { atom } from 'recoil';
const textState = atom({
  key: 'textState', // ユニークなID
  default: '', // デフォルト値
});
```

`key`にはユニークなID（他のAtomやSelectorと被らない値）を設定する必要があります。`default`にデフォルト値を設定することができます。

### Atomへのアクセス

コンポーネントからAtomにアクセスするためにはフックを使う必要があります。

#### 状態の読み取り

Atomから状態を読み取るには[`useRecoilValue`](https://recoiljs.org/docs/api-reference/core/useRecoilValue)フックを使うことができます。次の例では`Echo: Hello`の文字列が表示されると思います。

```tsx
import { atom, useRecoilValue } from 'recoil';

const textState = atom({
  key: 'textState',
  default: 'Hello',
});

export const CharacterCounter = () => {
  return (
    <div>
      <TextInput />
    </div>
  );
};

const TextInput = () => {
  // useRecoilValue()の引数にAtomを渡す
  const text = useRecoilValue(textState);
  return <div>Echo: {text}</div>;
};
```

#### 状態の書き込み

Atomへ状態を書き込むには[`useSetRecoilState`](https://recoiljs.org/docs/api-reference/core/useSetRecoilState)を使うことができます。次の例では`input`の値を書き換えることによって、Atomの値が更新されることを確認できます。

```tsx
import { atom, useRecoilValue, useSetRecoilState } from 'recoil';

const textState = atom({
  key: 'textState',
  default: 'Hello',
});

export const CharacterCounter = () => {
  return (
    <div>
      <TextInput />
    </div>
  );
};

const TextInput = () => {
  const text = useRecoilValue(textState);
  // useSetRecoilState()の引数にAtomを渡す
  const setText = useSetRecoilState(textState);
  return (
    <div>
      <input type="text" value={text} onChange={(e) => { setText(e.target.value) }}
      />
      <br />
      Echo: {text}
    </div>
  );
};
```

#### 状態の読み取りと書き込み

Atomから状態を読み取る、Atomへ状態を書き込む、両方の操作を行うフックとして[`useRecoilState`](https://recoiljs.org/docs/api-reference/core/useRecoilState/)を使うこともできます。`useRecoilState`の見た目は`useState`に似ているので、非常にとっつきやすいと思います。ただし引数に初期値ではなく、Atomを渡します。

```tsx
import { atom, useRecoilState } from 'recoil';

const textState = atom({
  key: 'textState',
  default: 'Hello',
});

export const CharacterCounter = () => {
  return (
    <div>
      <TextInput />
    </div>
  );
};

const TextInput = () => {
  // useRecoilState()の引数にAtomを渡す
  const [text, setText] = useRecoilState(textState);
  return (
    <div>
      <input type="text" value={text} onChange={(e) => { setText(e.target.value) }}
      />
      <br />
      Echo: {text}
    </div>
  );
};
```

## Selector

ドキュメントによるとSelectorは「derived state」と記述されていました。翻訳にかけてみると「派生した状態」という意味になるようです。

派生した状態とは「transformation」、つまり何かしらのAtomを変換・加工した値のことです。Selectorは純粋関数でAtom（および他のSelector）を変換・加工した出力と捉えることができます。

```tsx
import { selector } from 'recoil';
const charCountState = selector({
  key: 'charCountState', // ユニークなID
  get: ({get}) => {
    // 文字列を取得し、文字列の長さを返す
    const text = get(textState);
    return text.length;
  },
});
```

Atomと同様に`key`にはユニークなIDを設定する必要があります。上記の例では`get`を設定しています。`get`に渡された関数はAtomおよび他のSelectorにアクセスできます。

### Selectorへのアクセス

Atomと同様に、Selectorへのアクセスもフックを使う必要があります。

#### Selectorから派生状態を取得する

派生状態の取得には`useRecoilValue`を使用することができます。ここでAtomの状態の読み取りでも`useRecoilValue`を使用したことに気づくかもしれません。そうです、Atomの読み取りと同様のフックを使用して、Selectorの派生状態を取得することができます。

```tsx
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';

const textState = atom({
  key: 'textState',
  default: 'Hello',
});

const charCountState = selector({
  key: 'charCountState',
  get: ({get}) => {
    const text = get(textState);
    return text.length;
  },
});

export const CharacterCounter = () => {
  return (
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  );

// ...略

const CharacterCount = () => {
  // useRecoilValue()の引数にselectorを渡す
  const count = useRecoilValue(charCountState);
  return <>Character Count: {count}</>;
}
```

#### Selectorを通して状態を更新する

今回のコードでは登場しませんが、Selectorを通して加工した状態を書き込むことも可能です。ただし、Atomとは違い、Selectorは常に書き込み可能というわけではありません。

書き込み可能なセレクターを生成するためには、`set`プロパティを設定する必要があります。

https://recoiljs.org/docs/api-reference/core/selector#writeable-selectors

```tsx:setプロパティの例
const proxySelector = selector({
  key: 'ProxySelector',
  get: ({get}) => ({...get(myAtom), extraField: 'hi'}),
  set: ({set}, newValue) => set(myAtom, newValue),
});
```

Atomと同様に、書き込み可能なSelectorは`useSetRecoilState`や`useRecoilState`を使用して、加工した状態を書き込むことができます。



# おわりに

Recoilに入門してみるということで、AtomとSelectorの基本的な部分に関してドキュメントを読みつつまとめてみました。本記事で取り上げた内容は基本中の基本であり、Recoilのほんの一部しかすぎませんが、ドキュメントも読みやすく、APIもシンプルで、非常に使いやすく感じました。気になっているけどまだ触ったことないという方は、是非触ってみてください！