MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

React v18のSuspenseを振り返る

FrontEnd
May 27, 2022

GO BUSINESSのフロントエンド側を担当している林です。 この記事ではReact18で正式に実装されるSuspenseという機能について紹介します。


Suspenseとは

Suspenseという機能はReact16.6で実験的コンポーネントとして追加されましたが、React18では正式な機能として実装されることになりました。Suspenseはデータの受け取り状態を検知するコンポーネントであり、データを受け取るまではfallbackで指定した要素を返し、データを受け取るとSuspenseで囲っている子要素を返すという動きをします。 これまではサーバーサイドからのデータの受け取り状態は以下のようにuseStateやuseEffectなどのReact hookを利用して自身で状態管理することがほとんとでしたが、

import React, {useState, useEffect} from 'react'
import { Loading } from '../components/Loading'
import fetchViewer from '../api/fetchViewer'

const Header = () => {
  const [viewer, setViewer] = useState(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
     const viewer = fetchViewer()
     
     setViewer(viewer)
     setLoading(false)
  }, [])

  return {
    <h2>{viewer.displayName}</h2>
  }
}

const Page = () => {
  if (isLoading) return <Loading />
  
  return <Header />
}

Suspenseの利用によって以下のように状態管理を省略することができるようになりました。Headerコンポーネントの責務をデータの取得と表示のみに狭めることができ、ローディング状態などの管理はSuspenseコンポーネントへ投げるような構造になっていることがわかります。

import React, { Suspense } from 'react'
import { Loading } from '../components/Loading'
import fetchViewer from '../api/fetchViewer'

const Header = () => {
  const viewer = fetchViewer()

  return <h2>{viewer.displayName}</h2>
}

const Page = () => {
  return {
		 <main>
		   <Suspense fallback={<Loading />}>
		     <Header />
		   </Suspense>
     </main>
  }
}

Suspenseを使用する上でのデータ取得関数

Suspenseコンポーネントがfallbackに指定したコンポーネントを返すかどうか判定するためには、データ取得中(上のソースコードの例ではfetchViewer())にPromiseをthrowしてSuspenseコンポーネントでcatchする必要があります。 ですので、データ取得関数をわざわざそのような作りにする必要があるのですが、これは自身で実装しなくてもreact-queryなどのデータ取得用ライブラリが既に存在するのでこれらと一緒にSuspenseを使用するのが良さそうです。

import axios from 'axios'
import { useQuery } from 'react-query'

const fetchViewer = async () => {
   const { data } = await axios.get('https://xxxxxxxxxxxx')

   return data
}

export const useFetchViewer = () => {
   return useQuery({
      queryKey: ['viewer'],
      queryFn: fetchViewer
   })
}
import React, { Suspense } from 'react'
import { Loading } from '../components/Loading'
import { useFetchViewer } from '../../hooks/useFetchViewer'

const Header = () => {
  const { data } = useFetchViewer()

  return <h2>{data.viewer.displayName}</h2>
}

const Page = () => {
  return {
		 <main>
		   <Suspense fallback={<Loading />}>
		     <Header />
		   </Suspense>
     </main>
  }
}

複数コンポーネントでのデータ取得

ここまでの例では一つのHeaderコンポーネント内でのデータ取得のみに着目していましたが、実際には複数コンポーネント内でデータ取得をし、全ての取得が完了するまでローディング状態を出し続けるといったケースが多いかと思います。useStateとuseEffectのみでの実装を試みるとあらゆる通信での状態管理が求められてstateが複雑になりがちですが、Suspenseでの実装では以下のようにSuspenseコンポーネント内に全てまとめるだけになるのでよりソースコードがシンプルになります。

<Suspense fallback={<Loading />}>
   <Header />
   <Content />
</Suspense>

またSuspenseをネストさせて取得した順番に表示させることも可能です。

この場合、ローディング中に表示されるLoadingコンポーネントは常に一つなので、通信が完了した順に表示させたいがLoadingコンポーネントをあちこちに混在させたくない場合に有効です。

<Suspense fallback={<Loading />}>
   <Header />
   <Suspense fallback={<Loading />}>
     <Content />
   </Suspense>
</Suspense>

おわりに

いかがでしたでしょうか? やはりバージョンアップするごとに宣言的UIライブラリとして洗練されていくReactを追いかけるのは非常にワクワクします。今回はSuspenseコンポーネントの利用方法のみに焦点を当てた記事でしたが、この機能によってコンポーネントだけでなくディレクトリ設計も変わっていくことは間違いないと思いますし、その他の観点からも開拓されるのが楽しみです。


We're Hiring!

📢
Mobility Technologies ではともに働くエンジニアを募集しています。

興味のある方は 採用ページ も見ていただけると嬉しいです。

Twitter @mot_techtalk のフォローもよろしくお願いします!