新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Facebook公式のReactの状態管理ライブラリRecoilは、Reduxの代わりになりそう

Reactの状態管理はReduxが有名で、大衆迎合主義の自分としては、ご多分に漏れずReduxを使っています。

ただ、Reduxを使っていてしんどいなぁと感じるところがいくつかあります。

1つは、とにかくボイラープレートが多いことです。

ちょっとしたステート(変数)の追加や修正をするだけでも、かなりの量のコーディングが必要です。

もう1つは、Reduxはあくまでステートの保持および、ステートを参照しているコンポーネントの再描画伝達までのライブラリなので、ステートを非同期に更新する機能はなく、その辺りをユーザーが自分で実装しないといけないところです。

とはいえ、ReduxはあくまでFacebookが提案した状態管理のアイディア「Flux」を実装したに過ぎず、別にReduxが悪いわけではないんですけどね。

ただ、ReduxはFacebookが作ったものじゃないし、Facebookが自社のサービスでReduxを使っているとは思えないんですよ。

Facebookはあれだけ巨大なサービスを運営している、SPAのプロ中のプロですし、Facebookが状態管理を含む、SPA全体のフレームワークを作ってくれないかなぁと思っていました。

真打ち登場

そんな中、まだベータ扱いですが、Facebookから状態管理ライブラリRecoilが登場しました。

recoiljs.org

触ってみた結論から言うと

これです!欲しかったのは!

やりたい事だけを書けばよく、うんざりするようなボイラープレートもありません。

ステートの非同期更新をはじめ、SPAでよく使われる場面が考慮されていて、必要そうな機能が提供されていました。

以下、Recoilの簡単な説明や、感想などを書いていこうと思います。

Recoilとは?

Recoilとは、Facebookが作った、Reactの状態管理ライブラリです。

atomと呼ばれる、グローバル変数のようなものを作ることができ、そのatomを使うコンポーネントが、atomをサブスクライブ(参照)します。

そして、atomが更新されると、そのatomをサブスクライブしていたコンポーネントだけが再描画されます。

1つのatomを複数のコンポーネントが同時にサブスクライブできるので、ステートを一箇所に集約しつつ、必要なコンポーネントだけを効率よく再描画することができます。

ちなみに、Reduxは1つの巨大なステートツリーを作成するのですが、atomの粒度は細かく、変数・オブジェクトレベルで独立して作成されます。

atom

サンプルとして、カウンタをアップダウンするアプリを作ってみます。

Facebook公式のReactの状態管理ライブラリRecoilは、Reduxの代わりになりそう

import React from 'react';
import './App.css';
import {
    RecoilRoot,
    atom,
    useRecoilState,
} from 'recoil';

const countAtom = atom<number>({
    key: `countAtom`,
    default: 0,
});

function UpDown() {
    const [count, setCount] = useRecoilState(countAtom);
    return (
        <div>
            <button onClick={() => setCount(count - 1)}>Down</button>
            {count}
            <button onClick={() => setCount(count + 1)}>Up</button>
        </div>
    );
}

function App() {
    return (
        <RecoilRoot>
            <UpDown />
        </RecoilRoot>
    );
}

export default App;

パッと見でも、なんとなく何しているか分かるくらいシンプルですね。

atom作成

countAtomという、数を覚えておくatomを作成します。

引数にはデフォルト値と、識別キーを設定します。

Recoilの内部的にはatomの識別は変数名ではなく、この識別キーを使っているので、キーはユニークになる必要があります。

atomをサブスクライブ

コンポーネントがatomをサブスクライブするには、useRecoilState()でサブスクライブするatomを指定して行います。

すると現状のatomの値とatomを変更するための関数が取得できるので、後は取得した値を表示し、必要に応じて取得した関数で値を更新します。

複数のコンポーネントからサブスクライブ

先程のatomを他のコンポーネントでも使えるようにします。

といっても、先程と同様、atomを使いたいコンポーネント内でサブスクライブするだけです。

Facebook公式のReactの状態管理ライブラリRecoilは、Reduxの代わりになりそう

function ViewCount() {
    const [count, setCount] = useRecoilState(countAtom);
    return (
        <div>
            {count}
        </div>
    );
}

function App() {
    return (
        <RecoilRoot>
            <UpDown />
            <ViewCount />
            <ViewCount />
            <ViewCount />
        </RecoilRoot>
    );
}

これだけです!

たったこれだけで、ステートを複数のコンポーネントで参照して、そのステートが変更されると、それを参照しているコンポーネントだけが再描画されるようになります。

余計な記述も必要なく、したい作業だけを記述すればOKなので、コードがスッキリしてとても見通しがいいです。

ビジネスロジックを分離する

コンポーネントがatomを直接編集するのはあまりよろしくないので、コンポーネントからビジネスロジックを分離してみます。

これもRecoilなら簡単にできるのですが、その前にRecoil特有の注意点があります。

atomが使えるのはコンポーネント内だけ

atomはグローバル変数のようなものと言いましたが、実は<RecoilRoot>コンポーネントで囲まれているコンポーネント間でのみ共有されます。

しかも、atomが使えるのは関数コンポーネントの中でのみで、関数コンポーネントの外からatomを参照することはできません。

なので、atomをコンポーネント外で操作するには、汎用的なロジックを関数で定義しておき、それを関数コンポーネントの内から呼び出すことにより、コンポーネント内のatomにロジックを適用させます。

先程のUp・Downを、ロジックを分離して任意の値で行えるようにしたのが下記になります。

const addCountAtom: (a: CallbackInterface) => (a: number) => void = ({ snapshot, set }) => async (addValue: number) => {
    const current = await snapshot.getPromise(countAtom);
    set(countAtom, current + addValue);
};

function UpDown() {
    const [count, setCount] = useRecoilState(countAtom);
    const addCount = useRecoilCallback(addCountAtom);
    return (
        <div>
            <button onClick={() => addCount(-3)}>Down</button>
            {count}
            <button onClick={() => addCount(3)}>Up</button>
        </div>
    );
}

addCountAtom()がロジック部分で、現在のatom値を取得して、引数の数だけ足した値をatomにセットしています。

これをuseRecoilCallback()に渡すことにより、コンポーネントのatomを使って関数が実行されるようになります。

すごく長く見えますが、TypeScript型定義で長く見えているだけで、実態は2行だけです。

selector

Recoilにはatomに加え、selectorという値もあります。

selectorはそれ自身は値を保持せず、他のatomから計算によって求めた値を返します。

元となるatomが更新されると、selector値も再計算されます。

例えばカウントを100倍にした値を取るselectorを作りそれを表示してみます。

Facebook公式のReactの状態管理ライブラリRecoilは、Reduxの代わりになりそう

const countSelector = selector<number>({
    key: `countSelector`,
    get: ({ get }) => get(countAtom) * 100,
});

function ViewCount100() {
    const count = useRecoilValue(countSelector);
    return (
        <div>
            {count}
        </div>
    );
}

function App() {
    return (
        <RecoilRoot>
            <UpDown />
            <ViewCount100 />
            <ViewCount100 />
            <ViewCount100 />
        </RecoilRoot>
    );
}

selectorは変更する値が無いので、変更関数を取得しないuseRecoilValue()を使って値を取得します。

非同期更新

次に、値を非同期更新する方法を見てみます。

atomの非同期更新

1つ目はatomの更新で、前述のビジネスロジックの箇所です。

といっても、async/awaitが使われていることから分かるように、これ自身非同期関数なので、特に意識しなくてもそのままawaitで非同期処理が書けます。

例えば、1秒遅れて更新するロジックは下記になります。まんまですね。

const addCountAtom: (a: CallbackInterface) => (a: number) => void = ({ snapshot, set }) => async (addValue: number) => {
    const current = await snapshot.getPromise(countAtom);
    await new Promise((resolve) => {
        setTimeout(() => resolve(), 1000);
    });
    set(countAtom, current + addValue);
};

selectorの非同期更新

もう一つはselectorの更新で、selectorの定義のget()関数をasyncにすれば、データ取得に非同期処理が書けます。

例えば、1秒遅れて更新されるselectorは下記になります。これもまんまですね。

const countSelector = selector<number>({
    key: `countSelector`,
    get: async ({ get }) => {
        const count = get(countAtom);
        await new Promise((resolve) => {
            setTimeout(() => resolve(), 1000);
        });
        return count * 100;
    }
});

ただ、1点だけ注意があります。awaitで非同期処理する前に、全てのget()を呼び出して値を取得しておかないと無限ループでハマります。

更新待ち処理

ステートが非同期で更新される時、更新されるまでの間、ウエイトの表示など、ユーザーに何かしらの別表示をさせることはよくあります。

これにもRecoilはデフォルトで対応しています。

先程のselectorの非同期更新の場合、そのselectorを使うコンポーネントを<Suspense>で囲うことにより、selectorの値が更新されるまでの間は、<Suspense>が表示されるようになります。

そして、値が確定すると<Suspense>ではなく、本来のコンポーネントが表示されるようになります。

Facebook公式のReactの状態管理ライブラリRecoilは、Reduxの代わりになりそう

function App() {
    return (
        <RecoilRoot>
            <UpDown />
            <Suspense fallback={<div>Loading...</div>}>
                <ViewCount100 />
                <ViewCount100 />
                <ViewCount100 />
            </Suspense>
        </RecoilRoot>
    );
}

ただ、この機能は「<Suspense>で囲います」だけの簡単な話ではなく、あくまで現行バージョンのReactでできる最大限のことであって、裏では将来のReactで導入予定の「Concurrent Mode(並列モード)」に沿った仕様で動いています。

Concurrent Mode(並列モード)とは?

Concurrent Mode(並列モード)とは、将来Reactに導入される予定の、新しい描画モードです。

Reactの描画は、表示するデータが全て決まってから描画する、同期的な処理です。

データ取得中のウエイト表示処理は、事前にデータの状態を把握して、ウエイト表示を含む、その時点での表示内容を決めてから描画するということを、ユーザー独自に実装する必要がありました。

Concurrent Modeをざっくり言うと、表示するデータに「確定」「取得中」「取得失敗」の状態を持てるようにして、描画時にそれらの状態によって表示出し分けできるようにした、Reactの新しい描画モードです。

これをReactが標準機能として持つことにより、今まで面倒だったウエイト処理が、Reactのお作法に従って書くだけで簡単に実装できるようになります。

実はRecoilのデータは、上記の状態を持ったデータで、来るべきConcurrent Modeの時代にそのまま活躍できるよう設計されています。

以上が、Recoilの簡単な紹介になりますが、Recoilはシンプルで直感的で使いやすく、まさに欲しかったものでした。

Facebookの本気を見た気がした

以前、React Hooksがリリースされた時に、新しい状態管理機能が出たのかと期待したのですが、「関数コンポーネントにステータスを持たせられるようにしただけか」とガッカリしたことがありました。

www.kwbtblog.com

その時、既にクラスコンポーネントがあるのに、あえて何故このタイミングに関数コンポーネントなの?と思ったのですが、今回Recoilを触ってみて、むしろRecoilを実現するために、React Hooksを作ったのではと感じたほどです。

今まで、何でFacebookはFluxを提案しておきながら、本格的な状態ライブラリを作らないのか疑問だったのですが、その理由が分かったような気がしました。

従来のReactの延長では限界があり満足のいくものが作れないので、これまでの資産を捨てて、新しい機能を追加してまでして、Recoilを投入してきたように思えます。

そして、その改革の範囲は、関数コンポーネントだけにとどまらず、Concurrent Modeという、Reactの描画の根幹にまで及んでいます。

確かにこれらが揃えば、SPAで想定される大方の処理は、ユーザー独自の実装に頼ることなく、一連の機能を使うだけで実装できそうです。

今後もブラウザでのアプリ開発は「HTML+JavaScript」の時代が続きそうなことを考えると、安価なライブラリを提供するより、これまで築き上げてきたもをの壊してでも、使いやすいSPAライブラリ郡を作ろうというFacebookの本気を見た気がしました。

Reactを変更するのはFacebookにしかできないことなので、そこまでやってもらえるなんて嬉しい限りです。

感想など

ここには書ききれませんでしたが、Recoil・React Hooks・Concurrent Modeには、他にもSPAサイトを作るにあたって使いそうな機能が色々あって、サイト制作の視点でよく考えられてるなぁと思いました。

まだ発展途上ですが、Recoilは現行のReactでも十分快適に使えるかと思います。

今までは、何かちょっとしたサイトを作る際、本当はReactを使ってサクッとサイトを作りたかったのですが、そのコーディング量の多さに躊躇していました。

関数コンポーネントが主流になって随分コードがスッキリしましたし、これからはRecoilでサクッとサイトを作っていきたいですね!