先日Reactの次期バージョンReact 18のアルファがリリースされました 👏
このアルファバージョンを開発者に使ってもらいReact 18 Working Groupで議論・フィードバックを得ながら、ベータ、RC(リリース候補)を経て数ヶ月後に正式リリースされる予定です。React 18の新機能はIntroducing React 18や各種ブログ等を参照してください。
さて今回はSuspenseを復習してみます。SuspenseはReact 16.6から実験的機能としてReactに含まれていましたが、React 18で正式リリースになります。
https://www.geograph.org.uk/photo/4708402 より
データをAPIで取得して表示する
Reactはブラウザー上で動くJavaScriptが画面を表示する方式(アーキテクチャ)ですから、サーバーに格納されているデータを取得し表示するには教科書的に以下のようなコードで行います。
import React, { useState, useEffect } from 'react'
const URL = "APIサーバーのURL"
type MessageType = {message: string}
export const App = () => {
const [message, setMessage] = useState("")
const getMessage = async () => {
const response = await fetch(URL)
const data = await response.json() as MessageType
setMessage(data.message)
}
useEffect(() => { getMessage() })
return <h2>{message}</h2>
}
このコードがブラウザーに読み込まれ実行されると、以下の手順でサーバーから取得したメッセージが表示されます。
- ステートmessageが初期値の空文字列で作成される
return <h2>{message}</h2>
が実行され、<h2></h2>
がブラウザーに表示(初期表示)される- 初期表示が完了したので、useEffectに渡された、
getMessage()
を含む無名関数が実行される - getMessage関数が実行され、サーバーからデータを取得する
- ステートmessageにサーバーから取得したメッセージが設定される
- ステートが変化したのでAppコンポーネントが再評価され、
<h2>メッセージ</h2>
が表示される
loading…表示を追加
サーバーからのデータ取得には時間がかかるので、読み込み中 loading… 表示を追加した方がよいですよね。読み込み中表示を追加すると以下のようになります。
import React from 'react'
import { useState, useEffect } from 'react'
const URL = "APIサーバーのURL"
type MessageType = {message: string}
export const App = () => {
const [message, setMessage] = useState("")
const [loading, setLoading] = useState(true)
const getMessage = async () => {
const response = await fetch(URL)
const data = await response.json() as MessageType
setMessage(data.message)
setLoading(false)
}
useEffect(() => { getMessage() })
if (loading) {
return <p>loading...</p>
} else {
return <h2>{message}</h2>
}
}
読み込み中をloadingステートで管理し、その値で loading… 表示とメッセージ表示を切り替えています。
この方式の問題点
上のやり方の問題点は、まず面倒な事です。取得したデータや読み込み中をステートとして管理しなくてはいけませんし。データ取得が何度も起きないようにuseEffectも必要です。
また、サーバーから取得したメッセージが表示される手順に書いたように、表示が2回行われています。また、サーバーからのデータ取得と初期表示は平行できるはずですが、順番に実行されています。
これらの問題を解決するのがSusupenseです!
Susupenseを使うと
Suspenseを使ったコードは以下のようになります。getMessage関数が書かれていませんが、それは後で説明します。
React 16.6以降では、ReactにはSuspenseコンポーネントが組み込まれています。Suspenseでは通信・表示等を行うコンポーネント(ここでは <ShowMessage />
)をSuspenseコンポーネントの子要素にします。また読み込み中表示を行いたい場合はfallback属性で指定します。
ShowMessageコンポーネントはgetMessage()関数でサーバーからメッセージを取得し、<h2>
ダグ内に表示します。
import React, { Suspense } from 'react'
const URL = "APIサーバーのURL"
type MessageType = {message: string}
export const App = () => {
const ShowMessage = () => {
const response = getMessage() as MessageType
return <h2>{response.message}</h2>
}
return (
<Suspense fallback={<p>Loading...</p>}>
<ShowMessage />
</Suspense>
)
}
さて、getMessage()の説明用の実装は以下のようになります(注意:これは説明用の実装で実用的ではありません)。
let result:any = undefined
const getMessage = () => {
if (result === undefined) {
throw fetch(URL).then(r => r.json()).then(r => {result = r})
} else {
return result
}
}
getMessageを含む全体の実行時の流れを説明します、
- Suspenceはfallbackに指定された
<p>Loading...</p>
を表示し、子要素(ここではShowMessage)を実行します - ShowMessageが実行され、getMessage関数が呼び出されます。このさい変数resultの値はundefinedなので、通信を行うPromise(
fetch(URL).・・・
)がthrowされます - Suspenseはthrowされた値を(catch)捕まえ、そのPromise(ここでは通信)を実行します
- Promiseのthen()チェインの実行が進み、最後に通信結果が変数resultへ代入されます
- Promiseの実行が終わると、Suspenseはfallback表示を消し、再び子要素(ShowMessage)を実行します
- getMessage関数が再度呼び出されますが、変数resultに通信結果が入っているので、ShowMessageコンポーネントはその値を表示します
どうでしょうか? Suspenseはデータ取得関数(非同期処理)をthrowしてもらい、それをSuspenseコンポーネントがcatchする事で、非同期処理をReactの体系に上手く取り込んでいるように思えます。
ところで、実用的なgetMessageはどうやって作るんでしょうか? 答えはSupenseに対応したFetch的なHookを使えば良いのです。例えばSWR、React Query、useFetch などなど・・・ たくさんあります!
それってSWRで良いのでは?
はい。SWRはFetchだけでなくSuspenseと同じような事ができます。以下は今まで説明したコードをSWRで実装したものです。簡単ですね!
import React from 'react'
import useSWR from 'swr'
const URL = "APIサーバーのURL"
type MessageType = {message: string}
export const App = () => {
const { data } = useSWR<MessageType>(URL)
if (!data) {
return <p>loading...</p>
}
else {
return <h2>{data.message}</h2>
}
}
SWRをSuspenseに対応しているので以下のようになります、こちらも簡単ですね。
import React, { Suspense } from 'react'
import useSWR from 'swr'
const URL = "APIサーバーのURL"
type MessageType = {message: string}
export const App = () => {
const ShowMessage = () => {
const {data} = useSWR<MessageType>(URL, { suspense: true })
return <h2>{data?.message}</h2>
}
return (
<Suspense fallback={<p>Loading...</p>}>
<ShowMessage />
</Suspense>
)
}
React 18から始まるConcurrent Mode
現状ではSuspenseを使わなくてもSWRでじゅうぶんな気がします。
しかしSuspenseはReact 18から本格的に始まるConcurrent Modeの要素1のつです。 Reactのメージャーバージョンアップは機能追加より、カーネルの変更が主のように思えます。
React 16.8でHooksが導入されてReactは大きく変わりましたが、React 16.0がリリースされた時には想像もできませんでした。 Suspenseはこれから始まる新しいReactへの入り口ではないでしょうか、 Concurrent ModeでReactは新しいReactに変わっていくのかも知れません。