開催2018年6月19日、第69回となる「HTML5とか勉強会」が開催されました。今回のテーマは「UIフレームワーク最前線」。Angular、Vue.js、Reactという3つのフレームワークにおける第一人者が一同に集い、それぞれの最新動向やおすすめポイントを語ります。「Suspense」に登場したのは、koba04氏。17より追加される新機能「Suspense」を、デモを交えつつ解説しました。スピーカーkoba04Suspenseとは何か?koba04氏:よろしくお願いします。「Suspense」というタイトルで話していきたいと思います。Suspenseを聞いたことがある方はどれぐらいいますか?(会場挙手)数人ぐらいですね。ありがとうございます。最初に自己紹介をさせていただきます。「koba04」というアカウントでやっています。今はサイボウズで働いていて、フロントエンド周りいろいろやっています。OSS関連では、React系にコントリビュートしていることが多いです。最初にSuspenseとはなにかという話です。Abramovによって発表されたのが最初です。Slicing」というものがあります。そっちは低スペックなモバイルでもUIをブロッキングせずに更新できるようになる機能で、Suspenseは非同期の更新処理をいい感じに扱うための機能というイメージです。importなど、基本的にPromiseをprimitiveとして扱うようなもの。Promiseを返すようなものだったらだいたいなんでも扱えます。非同期の更新処理って面倒くさいなと思っているかと思います。Reduxとかで実装すると、Start、Success、Failureとかのアクションを3つぐらい発行してloadingのstateを管理してみたいな、「あぁ、つらい......」みたいな感じになると思います。(会場笑)あと、そこをちゃんとしようとすると面倒くさくて。ある場面としては、いろんなコンポーネント単位でLoadingのスピナーがバーって5〜6個出ちゃったりとか、APIが100msぐらいで返ってきてるのに「loading...」というのが一瞬だけちらっと出たりして、すごいジャンクな感じ。このあたりをちゃんとやろうとすると面倒くさいのはけっこう同意できる点じゃないかと思います。そのあたりを解決するための機能がSuspenseです。supportかと思うと思いますが、ComponentがPromiseを返すのをsupportするというわけではありません。Promiseを返すComponentだけsupportした場合でもこの問題は解決できません。子や孫のComponentが非同期の依存関係を持っているときは、全部親のComponentや、Loading出したいところまでリフトアップしてこないとダメで、それを手動でやるとするとだいぶ厳しい感じのコードになります。このUserListPageで、その非同期の処理が全部終わるまでは「Loading...」を出したいときも、その中のComponentのPromiseの依存を全部上に持ってこないとダメになってしまうところが問題です。Suspenseはどう動くのかというところです。Boundaryという機能が入りました。これはなにかというと、renderやライフサイクルメソッドでエラーが起きたときに、それを親のComponentのcomponentDidCatchというライフサイクルメソッドでキャッチして、エラー用の表示をするための機能です。これと同じ仕組みでSuspenseは動きます。Boundaryの場合はエラーオブジェクトをキャッチするんですが、Suspenseの場合はPromiseがthrowされるので、それをキャッチして。この場合もTimeoutというComponentでキャッチして処理するみたいなかたちになります。実際にはthenableなオブジェクトだったら大丈夫です。ここでUserというComponentがPromiseをthrowしたら、Timeoutでキャッチして。Timeoutは、指定されたミリ秒が経つかPromiseがresolveするかどちらかで、Timeoutが先に来たらfallbackの表示を出し、PromiseがresolveしたらUserを表示する、みたいなフローになります。Promiseのキャッシュをうまく扱う仕組みというので、もう1個要素があります。Promiseのキャッシュをうまく扱うための仕組みが必要になるんですが、そのためのsimple-cache-providerというライブラリもReactが提供しています。これはReactのリポジトリの中に入っていて、コード自体もかなり小さく、ほかの依存もないので、Reactのリポジトリの中にあるコードの中ではかなり読みやすいほうだと思います。最近はキャッシュにLRUの実装が入って400行ぐらいになったんですが、それでもかなり短いので、簡単に読めるコードになっています。こいつはキャッシュのコンテナみたいなイメージです。どのように遷移するかというと、最初はなにもないのでEmptyの状態になっています。そこでreadとかpreloadをすると、非同期処理のPromiseを作成します。するとPendingという状態になります。このときにreadしようとすると全部Promiseをthrowするかたちになります。Promiseがresolveしたら、それをデータをキャッシュとして持って、それ以降は同期的にデータを返すという挙動になります。使うのかというところなんですが、最初になにかキャッシュを作って、あとはResourceというPromiseの依存のもの。この場合だとfetchですね。このfetchをしたものとキャッシュを組み合わせてuserFetcherを作って、それをComponentのrenderメソッドの中で読みます。最初になにもデータがないときは、このFetcher.readでPromiseがthrowされるというかたちになって、レンダリングがここで止まります。このComponentはPlaceholderというComponentで囲まれていて、その中でさらにReact.TimeoutというComponentがキャッチします。この場合だと、200ms以上だったら「loading...」という表示をfallbackとして出す、という実装になっています。こんな感じで状態を明示的には管理せずに書けるようになったり、このTimeoutのところを柔軟に制御できるようになるのがSuspenseの機能です。importなども同じ感じで扱うことができます。Suspenseを使ったデモということで、ここからはデモをします。これは、APIのwaitやTimeoutの秒数を動的に切り替えられるような実装になっています。普通に表示すると......。これはユーザーのstar一覧のリポジトリを取ってくるアプリなんですけど、今見たように「loading...」がチラって一瞬出てきました。Suspenseを使うと例えば、1秒以上経ったら「loading...」を出したいということも可能です。逆にTimeoutまでにAPIのデータがloadしたら、「loading...」という表示は出ずに普通に出るという感じです。それで先ほどの問題が解決できます。キャッシュに入っている場合、同期的にデータが返ってきます。一瞬で表示させることができるようになります。別のユーザーだと、またloadingが走るというかたちになります。このように、非同期の更新処理の制御が柔軟にできます。koba04/react-suspense-demo)。このあたりはただAPIをfetchしているだけです。さっきのcreateCacheなどを使ってfetcherオブジェクトを作成して、getApiDataとしてラップして、こいつをこのComponentのrenderメソッドの中から普通に呼んでいるだけという実装だけです。1個は、これは画像を表示するケースです。普通に出すと、一瞬ガタガタっと表示されます。これに対する解決方法としては、画像のサイズを固定するという方法を取ると思います。すると、画像のサイズは固定されますが、ネットワークが遅い場合だと、先ほどのように画像が徐々に読み込まれます。回線を3Gぐらいにしておくとより顕著なんですけど、こんな感じにたぶん画像の読み込みが長くなります。これはたぶんみなさん許せないと思います。方法としてはpreloadします。preloadすると、この場合は、画像の表示が全部preloadが終わった後に行われるので、一瞬でパッといい感じに出ます。今のなにも出ないのが嫌だという人は、プレースホルダー画像の表示もできるようになっています。こんな感じにすると、プレースホルダー画像を表示して、loadが終わったら出すみたいなことが先ほどと同じ感じで出ます。koba04/react-suspense-demo)。今だとTimeoutのこのComponent自体が非同期のPromiseの投げるやつになっているんですけど。この場合は、なにか別のただのComponentのwrapperがあって、この中にPromiseを返すComponentが入っているんですけど。いうのって普通のPromiseのComponentサポートだと対応できないんですけど、そのへんもちゃんとできるようになっています。ImageWrapperというタイトルを出したいと思ったら、その外側のTimeoutのさらに中でTimeoutでキャッチすることもできて、そうするとこいつが内側で処理するので外側まで伝播しなくなります。この「ImageWrapper」というタイトルはちゃんと表示されたまま、その中で画像のpreloadの表示がされるようになります。エコシステムとの連携もという感じで、更新処理の制御はすごく柔軟にできるようになるというのがSuspenseの機能です。Slicingもそうなんですが、基本ネットワークが不便だったり、すごい低スペックな端末とか使っている人に向けての機能が、最近はすごく強化されているという印象です。いうわけで、今のは17でデフォルトで使えるようになる予定で、最終的なAPIはたぶんまだ変わります。DOMのcanaryとsimple-cache-providerをインストールすれば使えるようになっています。今のデモもGitHubにあって、Netlifyとかでデプロイしてるので、手元でも見れます。そのほかでは、サーバサイドでSuspenseを動かすという実験とかも行われていて。DOMとかをまったく使わずに今みたいな表示が行われています。このへんはソースが公開されていないので、どういうテイストになっているかはわかりませんが。あとは、ReactEuropeでは、先ほどのSuspenseをApolloのキャッシュと組み合わせるデモもあったりとかって感じで、けっこうそのあたりのエコシステムと組み合わせるということも盛り上がっている段階です。リリースは2018年中というわけでまとめです。先ほどの例は、最終的なAPIではないので変わります。さっき見たみたいに、renderメソッドの中ってこれまでって「副作用みたいのは書くな」というのが鉄則というかベストプラクティスだったんですが、それを完全に覆してfetchしちゃうみたいな感じになっているので、Componentのあり方は変わってくるんじゃないかと思います。あとは、先ほどのものを利用することでpreloadingをやったり、hiddenというpropsを使うことで、事前に裏側でメインのページを邪魔せずprerenderしておくみたいな、そういうのもできるようになったりします。ReduxやStoreとどう組み合わせるのか、そうしたエコシステムとどう組み合わせていくかは今後も注目していくという段階です。いつ使えるの?」というところで、一応予定としては2018年中に17がリリースされるイメージです。実際に使える段階になっていて、今はFacebookなどで実際に試したりしているような段階です。いうことで、Suspenseに備えていきましょう。ありがとうございました。22:43スピーカーの話が良かったらいいねしよう!HTML5とか勉強会HTML5とか勉強会に関する記事をまとめています。コミュニティをフォローすることで、HTML5とか勉強会に関する新着記事が公開された際に、通知を受け取ることができます。