ReasonReact から Recoil を使う
Facebook が新しい React 用の状態管理ライブラリを公開しました。 Recoil という名前です(ググラビリティ〜)。
というわけで、これを ReasonReact から使ってみます。
利用するライブラリのご紹介
知っている方は飛ばして結構です。
Recoil
Facebook 発の React 用状態管理ライブラリ。
状態管理を、 Atom という状態と Selector という変換処理(純粋関数……って書いてあるんですけど普通に副作用を含める事を想定しているように見えますね?)で行います。この、 Atom から Selector を通してコンポーネントまでデータを持ってくる一連の流れを「データフローグラフ」と表現しているようです。
状態の名前が Atom というのは何となく Clojure を思い出しますね。
Atom は React Hooks の useState に似ていますね。ただし、 Recoil の仕組みを利用して同じ状態を複数のコンポーネントから利用できます。
Selector は、 SQL のビューみたいな感じですね。更新処理を持たせる事ができるのも面白い。
Reason
皆さんご存知、 Facebook が提供している OCaml ベースの AltJS ですね。 BuckleScript という、見た目そのまま OCaml の AltJS を、文法のみ JavaScript 風味にしたもので、 AST レベルでは完全に OCaml なのが特徴です。
強い型安全性、高いインターオペラビリティ、 JavaScript に変換された後の効率の良さが特徴とされています。特に JavaScript との連携の容易さは強く意識されており、コンパイルされた後の JavaScript コードの可読性や、利用性の高さに注意を払って開発されている点が強みです。
最近の改善で、 OCaml のレコードを JavaScript のオブジェクトにマッピングするようになり、 JavaScript との連携はより強力になりました。
// 型定義
type user = {
id: int,
name: string,
};
let user1 = {id: 42, name: "John"};
let user2 = {id: 43, name: "Alice"};
// 関数定義
// 関数ヘッドでのパターンマッチと分解が可能。
// 文字列補間も利用できる(ちょっと冗長な書き方が必要だが……)。
let displayUser = ({id, name}) => {j|[$(id)] $(name)|j};
このコードが次のように変換されます(コメント部分は筆者。全体的に、ちょっと省略して順番をいじっています)。
// 型定義はコンパイル後には消えている。 OCaml は実行時に型情報を持たない。
// OCaml のレコードの定義が JavaScript のオブジェクトの定義で表現されている!
var user1 = {
id: 42,
name: "John"
};
var user2 = {
id: 43,
name: "Alice"
};
// ES5 互換の JavaScript を吐き出すので、アロー演算子や分割代入や文字列補間は利用されない。
function displayUser(param) {
return "[" + (String(param.id) + ("] " + (String(param.name) + "")));
}
すごい。
ReasonReact
React の Reason 向けポーティングです。
同じ Facebook だけあってか React の中の人が開発に関わるなどしており、コミュニティもなかなか活発で、見ていて楽しいです。
関数型言語と React との相性の良さを味わってください。
Recoil を ResonReact から利用する
ではその Recoil を、 ReasonReact から使ってみます。
Reason で JavaScript のライブラリを利用するのは非常に簡単なのですが、静的型付き言語なので、型定義を書いてやる必要があります。
実は Recoil の型定義については、既に @bloodyowl さんという方が書いていらっしゃるので、これを利用するのが早いでしょう。
が、今回は型定義を書く練習も兼ねて、必要な部分のみを自分で実装してみる事にします。
新しいライブラリに慣れるためにそのライブラリの型定義を書いてみるというのは、用意された関数に知悉するためにも良い行いかと思います。
Recoil の型定義
書いていきましょう。
Reason から JavaScript の関数や値を呼び出す時の型定義については、ここを参考にします。
Recoil.re
という名前のファイルを作り、そこに定義を書いていきます。
Recoil の型
まず、状態の型を作ります。この状態というのは、
- atom
- 書き込み可能な selector
のどちらかです。
本来は、 selector が書き込み可能か不可能かをきっちりと型で表現するべきではありますが、今回は簡便さを優先してそこを無視します。
以下のような定義になります。
type t('a)
型引数を一つだけ取る t
という型を定義します。その型が実際どういうものなのかは、一切書く必要がありません。
RecoilRoot
次に、 RecoilRoot コンポーネントを定義します。
module Root = {
[@react.component] [@bs.module "recoil"] external make: (~children: React.element) => React.element = "RecoilRoot";
};
外部ライブラリのコンポーネント導入はこの辺りを参考にしました。
ここでも、本来は initializeState という prop を取るのですが、今回は省略しています。
atom
そして、 atom を定義します。
type atomOption('a) = {
key: string,
default: 'a,
};
[@bs.module "recoil"] external atom: atomOption('a) => t('a) = "atom";
let atom = (~key, ~default) => atom({key, default});
atom の定義はこの辺りを参考にしました。
同名の関数を定義して、この二つのフィールドはラベル付き引数として取るようにしています。
(別に直接オブジェクトを引数に取っても良いのですが、ラベル付き引数の方がより「らしく」見えるのでこう書きました。)
selector
その次に selector を定義します。
type getter = {get: 'a. t('a) => 'a};
type selectorOption('a) = {
key: string,
get: getter => 'a,
};
[@bs.module "recoil"] external selector: selectorOption('a) => t('a) = "selector";
let selector = (~key, ~get) => selector({key, get});
今回は書込み可能な selector は無視して、読み取り専用の selector の型定義のみを作ります。
同じく定義はこの辺りを参考にしています。
getter の型定義の 'a.
は多相型注釈です。 この getter は利用される時に、同じ関数内で 'a
の型が異なる場合が考えられる為、特定の型に固定しておく事ができません。だから、多相型注釈を使う必要があったんですね。
この辺りは @bloodyowl さんのコードを参考にしました。
Hooks
最後に Hooks を定義しましょう。
[@bs.module "recoil"] external useRecoilState: t('a) => ('a, ('a => 'a) => unit) = "useRecoilState";
[@bs.module "recoil"] external useRecoilValue: t('a) => 'a = "useRecoilValue";
type setter('a) = 'a => 'a;
[@bs.module "recoil"] external useSetRecoilState: t('a) => setter('a) = "useSetRecoilState";
この辺りは特に説明することは無いですね。
公式サイトの Intro to External に書いてある通りにするだけです。
実装
では、 Recoil を利用する実装を書いていきましょう。
今回は、 Recoil の公式サイトに載っている小さなデモを実装してみます。
テキストボックスに文字列を入力すると、それと同じ文字列と、文字数とを表示してくれるというやつですね。
atom
atom 関数を使って状態を作りましょう。
let textState = Recoil.atom(~key="textState", ~default="");
特に型注釈はありませんが、きちんと Recoil.t(string)
型であると推論されます。
selector
同じく、 selector 関数で selector を作ります。
let charCountState = Recoil.selector(
~key="charCountState",
~get=({get}) => get(textState) |> String.length
);
こちらも特に型注釈無く、 Recoil.t(int)
型であると推論されます。
コンポーネント
では、コンポーネントを作って上の Recoil の atom や selector を使ってみましょう。
まず、文字列を入力するコンポーネントです。
module Input = {
[@react.component]
let make = () => {
let (textValue, setNameValue) = Recoil.useRecoilState(textState);
let onChange = React.useCallback(e => setNameValue(e->ReactEvent.Form.target##value));
<div> <input value=textValue onChange /> </div>;
};
};
次に、入力された文字列と、その文字数とを表示するコンポーネント。
module Show = {
[@react.component]
let make = () => {
let textValue = Recoil.useRecoilValue(textState);
let charCountValue = Recoil.useRecoilValue(charCountState);
<> <div> {j|Echo: $(textValue)|j}->React.string </div> <div> {j|Character Count: $(charCountValue)|j}->React.string </div> </>;
};
};
これらのコンポーネントを同一の RecoilRoot コンポーネントの中に入れる事で、二つの異なるコンポーネントの間で状態を共有する事ができます。
[@react.component]
let make = () => <Recoil.Root> <Input /> <Show /> </Recoil.Root>;
これで、 ReasonReact から Recoil を使う事ができました。
コードの全容はここに置いてあります。
感想と注釈
後半、ほとんど説明が無かった事に気付かれたかと思います。
何故かと言うと、簡単で、 説明するべき事が殆ど無い からです。
細かい文法の差異を除けば、 JavaScript とほぼ同じような書き方で使う事ができます。何を解説しろというのか、という話ですね。
このように、 Recoil は ReasonReact から全く違和感なく利用できます。
ただし、今回解説したのは本当に Recoil の上辺の部分だという点には注意してください。
Recoil には非同期処理を扱う為の機能なども含まれていますが、今回はそれに関しては全く触れていません。この辺りを ReasonReact で利用するとなると、 async/await を用いて書かれた Recoil のコードとはかなり異なった見た目になる事は間違いないでしょう。機能としては申し分なく利用できるはずですが。
また、 Recoil も開発中のライブラリなので、 API は今後も変更されていくかもしれません。大規模に利用するのはまだ得策では無いかもしれませんね。
とはいえ、 Reason も Recoil もとても面白いので、この記事を読んで興味を持てるのであれば、是非とも触ってみてください。