React HooksとReduxとProxy

Daishi Kato(@dai_shi)氏:よろしくお願いします。「React HooksとReduxとProxy」というタイトルで発表します。

自己紹介をさせていただきます。加藤大志といいます。

今、フリーランスプログラマーで、仕事募集中の状態です。最近はTwitterをがんばっていまして、TwitterとMediumとGitHubにいろいろ書いています。

React自体は「0.14」から使い始めました。最初に出てきた頃……createClassが出てきた時は、ぜんぜん魅力を感じませんでしたが、0.14からfunction componentsが使えるようになって、非常に魅力的に感じて使い始めました。

その時は「function componentsだけで書きたい」と思っていました。Reduxの相性がすごくよかったので、はじめはReduxで「もうLocal stateは一切使わないで、全部Global stateに入れてしまえ」というスタイルを採用していました。

React Hooksが出たわけですが、React Hooksはfunction componentsですべてが完結する仕組みです。もともとの経緯がそうなので、とても歓迎しています。

0.14が出たときにも「もうすぐできるよ」と言われていて、しばらく待ったらすぐにできるかなと思って、Reduxでがんばっていましたが、実際はだいぶかかりましたね。やっとReact Hooksが出てきて、晴れてfunction componentsだけで書けるようになって、うれしいかぎりです。

React Hooksが好き過ぎて、小さいライブラリをたくさん作っています。今は6個ぐらい作っていて、今日はそのうちの「react-hooks-easy-redux」というライブラリについて紹介します。

ReduxとReact Redux

そもそもReduxとReact Reduxは分けて考えるもので、Redux自体は独立した、Reactとは関係のないライブラリになっています。実際、99行で実装できるくらい小さいです。最初は本当に小さかったなと思います。

一方で、React ReduxはReduxとつなげるバインディングライブラリになっています。これは非常に複雑というか、Reactの中でReduxを使うためにパフォーマンスチューニングされています。

ただ、そのパフォーマンスをきちんと理解して使うにはObject Identityやselectorなどの理解が必要で、初心者にはちょっと難しいなと思います。例えば、Reactが初めての人に教えるとき、そこがないと進まないのはちょっとつらいなと感じたことがありました。React Reduxのパフォーマンスのよさはあると思いますが、別の手があってもいいんじゃないかなと思っていました。

そして、React Hooksが登場してから、いろんな人がReduxの代替ライブラリのようなものを一生懸命作っています。

2つぐらいタイプがあって、1つは、Reduxを使わずにHooksだけでなんとなく似たようなことをするというものです。もう1つは、Reduxを使いつつ、React Reduxは使わずに、それに代わるバインディングをHooksで実現するというものです。私はどっちもやっていますが、今日紹介するのは後者のほうです。

ここまでが導入で、残りは、(スライドを指して)だいたいこれが一般的にどうやって実装されているかという紹介と、その問題点を示して、解決策の候補と、Proxyを使った解決策をご紹介しようと思います。

一般的な実装例

まず導入として、ReduxのStoreが普通に作られる感じをイメージしてください。stateにcounterの数字とtextの文字列があるRedux Storeを作ったとします。

これをHooksで使ったときの1つのイメージが(スライドを指して)こちら。Counterコンポーネントがあって、stateを取ってくるhookとdispatchを取ってくるhookがあって、コールバックを作って、そのstateを表示したり、そのコールバックをイベントから呼び出したりするように書きます。

countとtextが2つ、stateの中に入っているんですけど、普通に何事も考えずにやると、このコンポーネントではCounterしか使っていないにもかかわらず、state.textが変更された場合もレンダリングされてしまいます。

ちなみに、先ほどの例を実現する素朴な実装は(スライドを指して)こんな感じです。それぞれのHooksの実装……といっても1個しかないですが、こんな感じで作ります。あとでまた資料を公開するので、見ていただけたらと思います。

ただ、これは参考程度というか、実際にこのまま使うことはありません。いろんな問題点があるので、使わないです。

変更検知問題

先ほど言いましたが、そもそも「変更検知問題」というものがあります。

Redux stateは、一般的には、アプリに大きく1個のGlobal stateを作るんですけれど、その一部の変更によって、ぜんぜん関係ないコンポーネント、stateの一部の使わないコンポーネントまでレンダリングすることは避けたい、という問題があります。

一方で、仮にすごく小さいアプリや限定的にReduxを使っている場合で、パフォーマンスがそんなに問題にならなければ、先ほどのような素朴な実装でもそこまで問題ないです。アプリの形態によると思いますけど、問題にならないこともあります。

解決法としては、selectorを指定する方法と、memoizeする方法と、auto-detectする方法があると思います。ほかにもあるかもしれませんが、今日はauto-detectを採用するかたちです。一応、ほかの方法も紹介しておこうと思います。

selectorは、いわゆるReact Reduxのconnectを使ったもので、ただそのままなんですけど、stateから部分集合を取り出すようなselectorの関数をhookに渡して、部分集合を取り出してそれを使う。この部分集合に該当しない場合はレンダリングしないという作り方をします。これが一番シンプルですね。

(スライドを指して)これはmemoizationを使ったケースです。全部取ってきてしまうけれど、変わった部分や自分が使うとわかっている部分を、input arrayで渡してメモする。それによって、実際は一瞬だけrender関数が実行されるんですけど、そこまでひどくないというか、パフォーマンスはそこそこ出ます。

これは完全ではないかもしれないです。ちょっと見にくいかなというところはありますし、あまり一般的ではなく、提案されているようなやり方ではないです。

最後のauto-detectですが、これは先ほど紹介した使い方と一緒です。stateを丸々持ってきて、stateのcounterを表示するだけのレンダリングで、解決策1でselectorを指定したものと同じような動作をしようというものです。

Proxy

「そんなことができるか?」という話なんですけど、Proxyというものがあって、オブジェクトの操作をトラップして処理を行います。

これを使うと、そのstateの中のどこか一部分が読まれて……または書かれてでもいいんですけど、そこが触られたときに、触られたということを記録しておいて、あとでstateから変更されたときに「触られたところに該当するかどうか」で、レンダリングするかしないかを判断できます。

このアプローチを採用してみようということで、先ほど紹介したライブラリ「react-hooks-easy-redux」を作りました。

1個だけ、依存ライブラリになるものが入っていて、proxyequalというライブラリです。Proxy自体は、1個のオブジェクトに対して適用するんですけど、それを深いオブジェクトに対して再帰的に適用してくれるものです。(スライドを指して)実際に、これを作りました。

ベンチマークテストをやってみて

Proxyは一応コストがかかるので、どれぐらいコストがかかるのかなと思ってベンチマークテストをしました。と言っても、たった1個のシンプルなベンチマーク用のアプリを使って動かしているだけなので、これがすべてではないんですけど、一例としては(スライドを指して)こうなります。

スライドの左上に出ているReact Redux v5.0.7の例が評価対象で、FPSが50ぐらいです。それに対して、今回私が作ったreact-hooks-easy-reduxのv0.8.0は、FPSが8.37。実際に触ってもわかるぐらい遅くなって、パフォーマンスが出ないという問題がありました。

いろいろと調べていったところ、先ほどご紹介したproxyequalの依存ライブラリに問題があることがわかりました。そして、proxyequalの作者と一緒にそのボトルネックを解消して、新しいバージョンを作って、今は38.96ぐらいのFPSが出るようになっています。

体感でももちろんわかるんですけど、もとから比べれば断然「マシ」です。これなら利用に堪えられるかなくらいにはなっていますね。

これで終わりなんですが、興味のある方はぜひ使ってみてください。

Reduxを使って、さっきみたいに簡単に書けるのかとか、custom hooksはどんなものなのかとか、ベンチマークテストをもっとやってみたいとか、そういう方がいらしたら、ぜひ見てみてください。

以上で発表を終わります。ありがとうございました。

(会場拍手)