はじめに
僕は以前、ずぼらによるずぼらのための React Suspense - Qiita という記事で(以下「前記事」)、React Suspense を React v16 で気軽に先取りする方法を紹介しました。
そして、その後、React v16.6 において React Suspense は標準搭載されました。公式にアナウンスされたのは lazy というモジュールの動的ロードに関わる部分のみですが、React Suspense の基本構造は前記事で紹介した通りですので、実質的に v16.6 以降では React Suspense が使えるようになっています。
もっとも、前記事と v16.6 ではコンポーネント名などが違っていますので、それに合わせて記事を書き直したほうがよいかな、と思いつつ、前述のように基本構造には変更がないため、やる気スイッチが押されないままでいました。
ところが、React v16.8 においていよいよ React Hooks が標準搭載され、僕のやる気スイッチが押されました。一見 React Suspense と React Hooks は無関係のように思われるかもしれませんが、React Hooks により function component が使いやすくなることで、React Suspense も恩恵を受けるのです。
それについて記事にしてみました。React Hooks と React Suspense に興味がある方はぜひ読んでみてください。
前記事のサンプルソースを書き直してみる
前記事のサンプルソースを v16.6 向けに書き直したソースは次の通りです。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>React Suspense Test @16</title>
</head>
<body>
<div id="app"></div>
</body>
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script>
// 再描画時に WordPressRestApi が unmount されえるため、
// キャッシュは WordPressRestApi の外側に確保する必要がある
let cache = null
// WordPress の Rest Api を使用するコンポーネントの例
const WordPressRestApi = props => {
if (cache) return cache // cache をレンダリング
// throw Promise
throw (async function() {
let text
try {
const res = await fetch(
'https://demo.wp-api.org/wp-json/wp/v2/pages/2',
{mode: 'cors', credentials: 'include'},
)
const json = await res.json()
console.log(json)
text = json.title.rendered
} catch (e) {
text = e.message
}
// <div>{text}</div>
cache = React.createElement('div', null, text)
})()
}
// <Suspense fallback={<div>Now Loading...</div>}>
// <WordPressRestApi />
// </Suspense>
const re = React.createElement(
React.Suspense,
{fallback: React.createElement('div', null, 'Now Loading...'), },
React.createElement(WordPressRestApi),
)
ReactDOM.render(re, document.getElementById('app'));
</script>
</html>
Polyfill が不要になり、コンポーネント名や fallback の指定方法も変わりましたが、Promise 型のオブジェクトを throw する、という React Suspense の基本構造に変更はありません。まず、この React Suspense の基本構造はしっかりと理解できるようにしておいてください。
React Hooks を用いて書き直してみる
上のソースや前記事のソースを見て、なんかもやもやする部分があった方はおそらくかなり力のある方だと思います。
ずばりといいますと cache の持ち方です。実践において、さすがに僕が提示したソースのようにグローバル変数を用いて cache を確保する方はいないでしょう。モジュールレベルのローカル変数にするか、親コンポーネント(wrapper component)のメンバ変数として cache を確保するのが通常ではないでしょうか。
そして、ご存知のように React Hooks は function component をすごく書きやすくしてくれます。したがって、React Hooks を使うと wrapper component を書くのがすごく楽になります。
React Hooks を使って cache の持ち方を工夫したソースの例です。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>React Suspense Test @16</title>
</head>
<body>
<div id="app"></div>
</body>
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script>
// WordPress の Rest Api を使用するコンポーネントの例
const WordPressRestApi = props => {
const cache = props.cache.current
if (cache) return cache // cache をレンダリング
// throw Promise
throw (async function() {
let text
try {
const res = await fetch(
'https://demo.wp-api.org/wp-json/wp/v2/pages/2',
{mode: 'cors', credentials: 'include'},
)
const json = await res.json()
console.log(json)
text = json.title.rendered
} catch (e) {
text = e.message
}
// <div>{text}</div>
props.cache.current = React.createElement('div', null, text)
})()
}
// cache を保持するためのラッパークラス
const WordPressRestApiWrapper = props => {
const cache = React.useRef(false)
// <WordPressRestApi cache={cache} />
return React.createElement(WordPressRestApi, {cache}, null)
}
// <Suspense fallback={<div>Now Loading...</div>}>
// <WordPressRestApiWrapper />
// </Suspense>
const re = React.createElement(
React.Suspense,
{fallback: React.createElement('div', null, 'Now Loading...'), },
React.createElement(WordPressRestApiWrapper),
)
ReactDOM.render(re, document.getElementById('app'));
</script>
</html>
useRef()
はこのようにクラスのメンバ変数のように使うこともできるので便利です(参照:Is there something like instance variables? - reactjs.org)。
(実は、僕は最初、このようなケースでも useState()
を使っていたのですが、よく考えると、state を変更するたびに再描画する必要がない今回のようなケースでは useRef()
が適していることに気づきましたので修正しました)
最後に
いかがでしたでしょうか。React Hooks により React Suspense も恩恵を受けることを感じていただけたでしょうか。「React Suspense はまだ一部の機能しか使えない」と思っている方は損をしているといっても過言ではないでしょう。
少しでも皆様の力になれればうれしいです。