フューチャー技術ブログ

Recoilドキュメント入門

RecoilはFacebookのMeta社製のReact用状態管理ライブラリです。この分野はFacebook自体がFluxという概念を発表してから、さまざまなライブラリが開発されてきました。

特に広く利用されたのがReduxです。Reduxはとても小さなライブラリにもかかわらず、Reactだけで解決しようとすると大変だったことがエレガントに解決できましたが、非同期がネイティブ対応でなくプラグインが必要だったり、ちょっとディープに使おうとするとたくさんのボイラープレートコードが必要になったり大変だったり、というのはありました。また、TypeScriptの普及する前の時代からのものなので、正しい型つけをするのが面倒だったり、といろいろ大変なところもありました。

Redux本体もそれらを改善したRedux-Toolkitというのも出していたりするのですが、Meta自体が開発しているライブラリで、今までのさまざまなライブラリの使い勝手が悪く手間暇かかっていたところを改善したものがRecoilです。

僕自身、「もうRedux-Toolkitをだいたい覚えたし、新しいのはいいかな」という気持ちでいたのですが、ドキュメントを見ると、かなり洗練されたデザインになっており、ドキュメントを読むだけでも「そうか、今まで認識してなかったけど、ここには設計上の見えない制約が課されていたのか」と、目から鱗な体験があるものでした。

Recoilのドキュメントには癖がある

だいたいのドキュメントというものは、先頭から読んでいけば理解できるものなのですが、Recoilのドキュメントはちょっと癖があり、まだ学習していないはずの概念がポロポロ出てきます。なんというかWikipediaの沼を彷徨っているような、そんな感じです。

どの分野もそうですが「一次情報が大事だ、まずはそこに当たれ」ということはよく言われますが、その一次情報がこんな感じで迷子になってしまいがちです。未知の概念を推測しながら読んでいくのは結構大変です。途中で挫折したり、結構重要そうなポイントを読み飛ばしたりしてしまう恐れがあります。

ということで、Recoilのドキュメントを読むための基礎知識をまとめてみます。本エントリーは「Recoil入門」ではなくて、「Recoilドキュメント入門」としているのはそのためです。

Recoilで一番大事なものはatom。ついでにselector

Recoilの構成要素として、atomselectorというのがよく出てきます。atomは値のストレージです。何個も作ることができます。例えば誕生日を保持する、といった感じです。例えば「ユーザー」みたいな単位でオブジェクトを保持させることもできます。

もう1つがselectorです。selectorも、利用するコンポーネントからするとatom()と区別はつきませんが、これは「関数的なストレージ」です。他のatomselectorの値を参照し、計算した上でキャッシュします。Vue.jsのcomputedが一番近いと思います。ReactだとuseMemo()ですね

名称未設定ファイル-ページ1.drawio.png

コンポーネントとRecoilのステート(atom/selector)との接続はフックを使って行います。ReactのuseState()は値と変更関数をセットで取得しますが、それ以外のも含めて、フックがいろいろあります。selectoratomから導出される関数なので値取得はできるが書き込みはできなかったりするので、接続先や用途によってフックを選びます。

フック 書き込み関数 リセット関数
useRecoilState()
useRecoilValue()
useSetRecoilState()
useResetRecoilState()

atomselectorの実装は簡単です。キーというアプリケーション全体でユニークな属性を与えないといけないという制約はありますが、それを除けばselectorが依存する状態さえ渡してあげれば、好きな場所で好きなように定義できます。上記の図の誕生日のatomと、それをもとにした年齢のselectorは次のようになります。

state.ts
import { atom, selector } from "recoil"

export const birthdayState = atom({
key: "birthday",
default: new Date(),
})

export const ageState = selector({
key: "age",
get: ({get}) => {
const bd = get(birthdayState)
const today = new Date()
const thisYearsBirthday = new Date(today.getFullYear(), bd.getMonth(), bd.getDay());
if (today < thisYearsBirthday) {
return today.getFullYear() - bd.getFullYear() - 1
} else {
return today.getFullYear() - bd.getFullYear()
}
}
})

利用する側のコードはこんな感じです。初期の誕生日の値はフューチャーの創業日にしてます。33歳ですね。僕まだ30周年ってロゴの入った名刺を使い切ってないのですが。

App.tsx
import './App.css'
import { useCallback, useEffect, ChangeEvent } from "react"
import { useRecoilValue, useRecoilState } from "recoil"
import { birthdayState, ageState } from './store'

export function App() {
const [birthday, setBirthday] = useRecoilState(birthdayState)
const age = useRecoilValue(ageState)

// 初期化
useEffect(function init() {
setBirthday(new Date(1989, 10, 28))
}, [])

// テキストボックスが変更されたら呼ばれるコールバック
const onChange = useCallback(function changeState(ev: ChangeEvent<HTMLInputElement>) {
setBirthday(new Date(ev.target.value))
}, [])

return (
<div className="App">
<label htmlFor="birthday">誕生日:</label>
<input type="date" id="start" name="birthday" value="1989-11-28" onChange={onChange}></input>

<div>誕生日 {birthday.toString()}</div>
<div>年齢 {age}歳</div>
</div>
)
}

atomFamily, selectorFamilyとは

atomselectorに似たatomFamilyselectorFamilyがあります。ドキュメントを読んでいると説明なく出てくるのですが、これはatomselectorのファクトリー関数です。

例えば、家族全員の情報を入力するパネルを作りたいとします。家族の人数は可変です。徳川11代将軍の徳川家斉の情報を入力するには53人分の子供欄が必要です。そのような時に、atomFamily, selectorFamilyを使うと、子どもIDなどをもとにバリエーションを簡単に増やせます。先ほどのモデルに、数値でIDを渡してバリエーションを作るのを図示したのが次の絵です。別のコンポーネントから使う場合も、1つ実装すれば使いまわせます。

名称未設定ファイル-ページ2.drawio.png
export const birthdayFamilyState = atomFamily({
key: "birthday",
default: new Date(),
})

export const ageFamilyState = selectorFamily({
key: "age",
get: (p: number) => ({get}) => {
const bd = get(birthdayFamilyState(p))
const today = new Date()

const thisYearsBirthday = new Date(today.getFullYear(), bd.getMonth(), bd.getDay());

if (today < thisYearsBirthday) {
return today.getFullYear() - bd.getFullYear() - 1
} else {
return today.getFullYear() - bd.getFullYear()
}
}
})

xxxFamilyにパラメータを渡すと、atom/selectorが出てきます。利用するコンポーネントでパラメータp(ここではchildIdを設定)を入れると、コンポーネントごとにstateが持てます。Reduxでは地味に面倒だったやつ。

ShowBirthday.tsx
import { useRecoilValue } from "recoil"
import { birthdayFamilyState, ageFamilyState } from './store'

type Props = {
childId: number;
}

export function ShowBirthday({childId}: Props) {
const birthday = useRecoilValue(birthdayFamilyState(childId))
const age = useRecoilValue(ageFamilyState(childId))

return (
<div className="App">
<div>誕生日 {birthday.toString()}</div>
<div>年齢 {age}歳</div>
</div>
)
}

スナップショット

他にドキュメントを読んでいるとこれまた説明なしに出てくるのがスナップショットです。atomselectorは定義側は独立して作れますが、実態としてはkeyをキーにして、まとまった状態として管理されます。その状態をまるごと取り出したり、書き込んだりする機能がスナップショットのようです。テスト目的でまるごと状態を差し替えたり、といった用途に使えるようです。

おまけ: Redux-Toolkitとの違い

Reactで関数コンポーネントを使い、Redux-Toolkitとの接点でフックを使う前提で話をします。最初のReduxやHOCの使い方については触れません。

Redux-Toolkitの場合は、configureStore()を使ってストアという大きな箱を1つ作ります。このストアの中にはcreateSlice()を使って、サブのストアを作ります。Recoilはatom()をどこでも定義できて自由に使えますが、Redux-Toolkitではこのストア<Provider>コンポーネントに渡し、Reactアプリケーションの根っこの部分にリンクさせる必要があります。

コンポーネントとの接点はフックを使います。useSelector()でストアの中の状態のうち、コンポーネントが必要のある部分だけを選択してコンポーネントから参照できるようにします。また、useDispatch()を使ってストアの更新を行います。この時の呼ぶ処理は「Reducer」と呼ばれます。関数型チックな用語が使われていますが、元々のReduxは

 古い状態→Reducer→新しい状態

という感じで状態を変更していました。状態も純粋関数で変更するという形式です。まあさすがに潔癖すぎたのか、Redux-ToolkitのSliceではオブジェクト指向にちょっと回帰した感じのAPIになっています。 

名称未設定ファイル-ページ3.drawio.png

Redux-ToolkitはRecoilと比べると、selector的なものがありません。おそらく、次のどれかで代替する必要があります。

  • ちょっとした計算だったら毎回計算してしまえ
  • Reduxの外でuseMemo()を使う
  • 計算結果もストアに入れてしまう

ストアにはアプリケーション全体の状態が集まってしまい、密結合になってしまいます。Slice単位では再利用はReduxよりかはしやすい感じですが、コンポーネントが特定のパスにマウントされているSliceに依存する感じになりがちで、コンポーネントの再利用製がやや低くなってしまうのだな、とRecoilと比較すると思います。

ただ、状態一元管理されており、ストアとコンポーネント間の読み書きの流れもフックで隔離されているし、コード解析して状態を知るにはRedux-Toolkitの方が追いかけやすいと思うので、超大人数でアプリ開発するエンプラ開発のノリだとこっちの方が良いのかも?とか思ったりはします。きちんと分析して、アプリの持つ状態の構造とか全体像がきちんと把握できるのであればそこまで悪くはないと思います。うちはVue.jsを使う会社なのでRedux-Toolkitは使いませんが、フューチャーぐらい設計をしっかり固める会社ならこちらの方が向いているかな、と。

まとめ

Recoilはまだバージョンが1.0になっておらず、リファレンスを見るとUNSTABLEがついた関数もたくさんあります。ですが、今回触れなかった非同期の対応などの柔軟さ、エラー処理、Suspense対応など、Reactとの親和性が高まっており、今まで不便と感じてなかった部分についても「こんな改善がありえたのか」という発見がある、楽しいライブラリになっています。

ですが、ドキュメントもまだ発展途上なのか、説明が十分にない単語が堂々と出てきたりして、読んでいると不安になるところも少しあります。多くの人がすでにわかっていると思うRedux-Toolkitとの比較していますし、本エントリーで、そういうところの不安の解消ができれば、と思います。