Recoilを使ってみよう

5月頃になりますが、Reduxの代わりになるRecoilというパッケージがfacebook社の方からリリースされました。現在シェア率も伸びていて、日本では知名度がありませんが、海外ではRecoilがReduxを上回ったようです。
今後日本でも流れが来そうですね。

自分自身まだ技術検証をしていなかったのでチュートリアルを消化するついでにReduxとの比較もしていこうと思います。
先にRecoilのGet Startedのチュートリアルをさらっとやってみます。

Recoilの実装

$ create-react-app app
$ cd app
$ yarn add recoil
ESLintの設定

チュートリアルではESLintの設定を推奨しています。

{
  "plugins": [
    "react-hooks"
  ],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": [
      "warn", {
        "additionalHooks": "useRecoilCallback" // 
      }
    ]
  }
}

useRecoilCallback関数の依存関係が正しく指定されていない場合にESLintがwarningを出して、依存関係の修正をサジェストするようにしています。

では実際にコードを書いていきましょう。

App.js

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
        {/* ここにコンポーネントを書いていく */}
    </RecoilRoot>
  );
}

export default App;

RecoilRootがReduxでいうところのProviderの役割を果たしているようです。
ReactであればApp.jsなんかの共通で読み込まれる箇所に書いておくとよいでしょう、Next.jsであれば_app.jsあたりですね。
次にRecoilにおけるAtomとSelecterを見ていきましょう。

Atom

Recoilは、Atomと呼ばれるものを使用して状態管理をします。内部ではuseStateと全く同じAPIが動いています。そのためuseStateと同じ感覚で使用ができます。useStateと違うのはAtomはユニークキーで識別している点です。

const textState = atom({
  key: 'textState', // キーになるユニークな文字列を入力してください、selecterで取得する際に使用します
  default: '', // 初期値になります。
});

そしてこのatomとセットで覚えておくのがuseRecoilState関数。先ほど定義したAtomを引数に渡すことでReactのStateとして管理できるようになります。

const [text, setText] = useRecoilState(textState);

使い方としてはuseStateと一緒で配列にstateとdispatchが入ってきます。
では次にSelectoruseRecoilValueをみていきましょう。

Selector

const charCountState = selector({
  key: 'charCountState', // unique ID (with respect to other atoms/selectors)
  get: ({get}) => {
    const text = get(textState);

    return text.length;
  },
});

selectorで定義した値を読むときはどのようにするのか?そこでuseRecoilValueを使用することで値を取得することができます。

const count = useRecoilValue(charCountState);

全体のコード

atom/selectorを用いて、実際に書いたコードがこちらになります。

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {

  const textState = atom({
    key: 'textState', // unique ID (with respect to other atoms/selectors)
    default: '', // default value (aka initial value)
  });

  const charCountState = selector({
    key: 'charCountState', // unique ID (with respect to other atoms/selectors)
    get: ({get}) => {
      const text = get(textState);

      return text.length;
    },
  });

  function CharacterCounter() {
    return (
      <div>
        <TextInput />
        <CharacterCount />
      </div>
    );
  }

  function TextInput() {
    const [text, setText] = useRecoilState(textState);

    const onChange = (event) => {
      setText(event.target.value);
    };

    return (
      <div>
        <input type="text" value={text} onChange={onChange} />
        <br />
        Echo: {text}
      </div>
    );
  }

  function CharacterCount() {
    const count = useRecoilValue(charCountState);

    return <>Character Count: {count}</>;
  }

  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

export default App;

課題点など

Recoilを使用することでuseStateを使用する感覚でグローバルに値を保持できる。Fluxの概念が必要なくなるので、コードがかなり書きやすくなるのと、Reduxに比べてファイル数が減りますね。
Fluxでコードを書かずにさらっと実装したいケースなんかは良いかもしれません。
またutilも充実しているので、非同期処理周りのハンドリングも行えそうです。

試していませんが、useReducerなんかと組み合わせればReduxみたいにかけるのかな?なんて考えたりもしています。そこらへんは別で検証してみたいと思っています。
物自体かなりシンプルなので使い方次第ではいろいろと作れそうな気がしています。 

ただ、現段階でSSRに対応し切れていないので実務に導入するのはまだ先になりそうです。CSRでのアプリケーション開発では入れてもよいかもしれませんが。情報資産がないのも今のところネックです。

また、ステートをユニークキーで管理するのでこの辺は設計の段階でどう管理していくかを決めておかないと規模が大きくなった時に辛くなりそう。atom/selectorの管理をどのようにしていくのか、ディレクトリ構造含め、設計していく上での課題になりそうです。

結論。今後に期待。

関連記事