EY-Office ブログ

React 18への予習シリーズ:Suspenseを復習しよう

先日Reactの次期バージョンReact 18のアルファがリリースされました 👏
このアルファバージョンを開発者に使ってもらいReact 18 Working Groupで議論・フィードバックを得ながら、ベータ、RC(リリース候補)を経て数ヶ月後に正式リリースされる予定です。React 18の新機能はIntroducing React 18や各種ブログ等を参照してください。

さて今回はSuspenseを復習してみます。SuspenseはReact 16.6から実験的機能としてReactに含まれていましたが、React 18で正式リリースになります。

Suspense 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>
}

このコードがブラウザーに読み込まれ実行されると、以下の手順でサーバーから取得したメッセージが表示されます。

  1. ステートmessageが初期値の空文字列で作成される
  2. return <h2>{message}</h2>が実行され、 <h2></h2>がブラウザーに表示(初期表示)される
  3. 初期表示が完了したので、useEffectに渡された、getMessage()を含む無名関数が実行される
  4. getMessage関数が実行され、サーバーからデータを取得する
  5. ステートmessageにサーバーから取得したメッセージが設定される
  6. ステートが変化したので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を含む全体の実行時の流れを説明します、

  1. Suspenceはfallbackに指定された <p>Loading...</p> を表示し、子要素(ここではShowMessage)を実行します
  2. ShowMessageが実行され、getMessage関数が呼び出されます。このさい変数resultの値はundefinedなので、通信を行うPromise(fetch(URL).・・・)がthrowされます
  3. Suspenseはthrowされた値を(catch)捕まえ、そのPromise(ここでは通信)を実行します
  4. Promiseのthen()チェインの実行が進み、最後に通信結果が変数resultへ代入されます
  5. Promiseの実行が終わると、Suspenseはfallback表示を消し、再び子要素(ShowMessage)を実行します
  6. getMessage関数が再度呼び出されますが、変数resultに通信結果が入っているので、ShowMessageコンポーネントはその値を表示します

どうでしょうか? Suspenseはデータ取得関数(非同期処理)をthrowしてもらい、それをSuspenseコンポーネントがcatchする事で、非同期処理をReactの体系に上手く取り込んでいるように思えます。

ところで、実用的なgetMessageはどうやって作るんでしょうか? 答えはSupenseに対応したFetch的なHookを使えば良いのです。例えばSWRReact QueryuseFetch などなど・・・ たくさんあります!

それって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に変わっていくのかも知れません。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ