React + TypeScript: Facebookの状態管理ライブラリRecoilを使ってみる

[Recoil](https://recoiljs.org/)はReactの状態を管理するライブラリで、Facebook改めMetaが開発しています。本稿執筆時の[最新バージョン](https://github.com/facebookexperimental/Recoil/releases)は0.7.7です。状態をひとまとめにするのではなく、ひとつひとつ細かく分けて管理します。ざっくりとお伝えするなら、`useState`の状態をコンポーネントツリー内にまたがって共有するイメージです。

本稿は公式サイトの「[Getting Started](https://recoiljs.org/docs/introduction/getting-started)」で紹介されたコードサンプルを、モジュールに分けた作例に書き替えて解説します(サンプル001)。また、TypeScriptによる型づけも加えました。

#### サンプル001■React + TypeScript: Recoil example
https://codesandbox.io/embed/react-typescript-recoil-example-fqlggn?fontsize=14&hidenavigation=1&theme=dark


# React + TypeScriptのひな形アプリケーションをつくる
Reactアプリケーションのひな形は、[Create React App](https://ja.reactjs.org/docs/create-a-new-react-app.html#create-react-app)でつくることにします。オプションとして`--template typescript`を加えれば、TypeScriptの環境が簡単に加わえられて便利です(「[Create React AppでTypeScriptが加わったひな形アプリケーションをつくる](http://www.fumiononaka.com/Business/html5/FN2006001.html#sec01)」参照)。


# インストール
Recoilは、npmあるいはyarnでつぎのようにインストールしてください。

```terminal
npm install recoil
```

```terminal
yarn add recoil
```


# コンポーネントツリーを`<RecoilRoot>`でつつむ
まず、状態を共有したいコンポーネントツリーのルートは、[`<RecoilRoot>`](https://recoiljs.org/docs/api-reference/core/RecoilRoot)で包んでください。すべての子孫コンポーネントから、同一の状態が使えるようになります。ここでは、親コンポーネントを`CharacterCounter`としました。

```react:src/App.tsx
import { RecoilRoot } from 'recoil';
import { CharacterCounter } from './CharacterCounter';

export default function App() {
	return (
		<RecoilRoot>
			<CharacterCounter />
		</RecoilRoot>
	);
}
```


# `atom`で状態を定める
[`atom()`](https://recoiljs.org/docs/api-reference/core/atom)は、状態をひとつひとつ定める関数です。`<RecoilRoot>`に包まれたツリー内のすべてのコンポーネントから、同じ状態の値が読み書きできます。`atom`の値を参照するコンポーネントは、暗黙的に購読対象となる仕組みです。`atom`が更新されると、購読対象のコンポーネントは再描画されます。`atom`に渡すオプションオブジェクトには、一意の識別子`key`とデフォルト値`default`を与えてください。

```react:src/textState.ts
import { atom } from 'recoil';

export const textState = atom({
	key: 'textState', // 他のatomやselectorに対して一意のID
	default: '', // デフォルト値(初期値)
});
```

`<RecoilRoot>`に包んだ親コンポーネント`CharacterCounter`には、このあとふたつの子コンポーネント(`TextInput`と`CharacterCount`)を定めて加えます。

```react:src/CharacterCounter.tsx
import { TextInput } from './TextInput';
import { CharacterCount } from './CharacterCount';

export const CharacterCounter: React.FC = () => {
	return (
		<div>
			<TextInput />
			<CharacterCount />
		</div>
	);
};
```

コンポーネントから`atom`の読み書きをするために用いるのが[`useRecoilState()`](https://recoiljs.org/docs/api-reference/core/useRecoilState)フックです。構文は`useState`と同じで、コンポーネントツリーの外にある状態を共有して使えます。

状態がコンポーネントの外にあることを除けば、`useState`フックとやっていることは変わりません。`<input type="text">`要素に入力した値が設定関数`setText()`で状態を書き替え、状態変数`text`の値はテキストとして表示されます。

```react:src/TextInput.tsx
import { useCallback } from "react";
import { useRecoilState } from 'recoil';
import { textState } from './textState';

export const TextInput: React.VFC = () => {
	const [text, setText] = useRecoilState(textState);
	const onChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
		({ target: { value } }) => {
			setText(value);
		},
		[setText]
	);
	return (
		<div>
			<input type="text" value={text} onChange={onChange} />
			<br />
			Echo: {text}
		</div>
	);
};
```


# `selector`で状態の値に手を加えて返す
複数の`atom`から参照した値に手を加えて返すのが[`selector()`](https://recoiljs.org/docs/api-reference/core/selector)です。他の`selector`から値を得ることもできます。派生の状態をつくる純粋な関数です。引数として渡すオプションオブジェクトの`get()`に定めたコールバックは、状態の値にもとづいて処理した結果を返します。コールバックの引数(`get`)の`get()`が`atom`(`textState`)から値を参照するための関数です。

`charCountState`は`textState`から`text`を得て、その文字数を返します。

```react:src/charCountState.ts
import { selector } from 'recoil';
import { textState } from './textState';

export const charCountState = selector({
	key: 'charCountState', // 他のatomやselectorに対して一意のID
	// 状態の値にもとづいて処理した結果を返す
	get: ({ get }) => {
		const text = get(textState);
		return text.length;
	},
});
```

`selector`の返す値を読み込むには、フック[`useRecoilValue()`](https://recoiljs.org/docs/api-reference/core/useRecoilValue)を用いてください。コンポーネント`CharacterCount`が表示するのは、`textState`に設定された文字数です。でき上がった作例の動きは、冒頭のサンプル001でご確認いただけます。

```react:src/CharacterCount.tsx
import { useRecoilValue } from 'recoil';
import { charCountState } from './charCountState';

export const CharacterCount: React.FC = () => {
	const count = useRecoilValue(charCountState);
	return <>Character Count: {count}</>;
};
```