RecoilはReactの状態を管理するライブラリで、Facebook改めMetaが開発しています。本稿執筆時の最新バージョンは0.7.7です。状態をひとまとめにするのではなく、ひとつひとつ細かく分けて管理します。ざっくりとお伝えするなら、useStateの状態をコンポーネントツリー内にまたがって共有するイメージです。
本稿は公式サイトの「Getting Started」で紹介されたコードサンプルを、モジュールに分けた作例に書き替えて解説します(サンプル001)。また、TypeScriptによる型づけも加えました。
サンプル001■React + TypeScript: Recoil example
React + TypeScriptのひな形アプリケーションをつくる
Reactアプリケーションのひな形は、Create React Appでつくることにします。オプションとして--template typescriptを加えれば、TypeScriptの環境が簡単に加わえられて便利です(「Create React AppでTypeScriptが加わったひな形アプリケーションをつくる」参照)。
インストール
Recoilは、npmあるいはyarnでつぎのようにインストールしてください。
npm install recoil
yarn add recoil
コンポーネントツリーを<RecoilRoot>でつつむ
まず、状態を共有したいコンポーネントツリーのルートは、<RecoilRoot>で包んでください。すべての子孫コンポーネントから、同一の状態が使えるようになります。ここでは、親コンポーネントをCharacterCounterとしました。
import { RecoilRoot } from 'recoil';
import { CharacterCounter } from './CharacterCounter';
export default function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
atomで状態を定める
atom()は、状態をひとつひとつ定める関数です。<RecoilRoot>に包まれたツリー内のすべてのコンポーネントから、同じ状態の値が読み書きできます。atomの値を参照するコンポーネントは、暗黙的に購読対象となる仕組みです。atomが更新されると、購読対象のコンポーネントは再描画されます。atomに渡すオプションオブジェクトには、一意の識別子keyとデフォルト値defaultを与えてください。
import { atom } from 'recoil';
export const textState = atom({
key: 'textState', // 他のatomやselectorに対して一意のID
default: '', // デフォルト値(初期値)
});
<RecoilRoot>に包んだ親コンポーネントCharacterCounterには、このあとふたつの子コンポーネント(TextInputとCharacterCount)を定めて加えます。
import { TextInput } from './TextInput';
import { CharacterCount } from './CharacterCount';
export const CharacterCounter: React.FC = () => {
return (
<div>
<TextInput />
<CharacterCount />
</div>
);
};
コンポーネントからatomの読み書きをするために用いるのがuseRecoilState()フックです。構文はuseStateと同じで、コンポーネントツリーの外にある状態を共有して使えます。
状態がコンポーネントの外にあることを除けば、useStateフックとやっていることは変わりません。<input type="text">要素に入力した値が設定関数setText()で状態を書き替え、状態変数textの値はテキストとして表示されます。
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()です。他のselectorから値を得ることもできます。派生の状態をつくる純粋な関数です。引数として渡すオプションオブジェクトのget()に定めたコールバックは、状態の値にもとづいて処理した結果を返します。コールバックの引数(get)のget()がatom(textState)から値を参照するための関数です。
charCountStateはtextStateからtextを得て、その文字数を返します。
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()を用いてください。コンポーネントCharacterCountが表示するのは、textStateに設定された文字数です。でき上がった作例の動きは、冒頭のサンプル001でご確認いただけます。
import { useRecoilValue } from 'recoil';
import { charCountState } from './charCountState';
export const CharacterCount: React.FC = () => {
const count = useRecoilValue(charCountState);
return <>Character Count: {count}</>;
};