ReactでfetchとSuspenseを使用してloading…を表示する方法

fetchとSuspenseでloading…を表示

Reactでfetchを使用してデータを読み込み中に「loading…」と表示させたいときは、以下のようにuseStateで読み込み中のフラグの変数を作成して、条件分岐で「loading…」を表示させているケースが多いです。

TSX
const [isLoading, setIsLoading] = useState(false)
TSX
return (
  <>
    <div>
      <button onClick={onClickHandler}>データを取得する</button>
    </div>
    <div>
      {isLoading ? (
        <p>loading...</p>
      ) : (
        products.map((product) => (
          <p key={product.id}>{product.id} {product.title}</p>
        ))
      )}
      {isError && <p style={{ color: 'red' }}>エラーが発生しました</p>}
    </div>
  </>
)

しかし、現在のReactにはSuspenseというコンポーネントがレンダリングされる前に、非同期操作が完了するまで待つ機能があります。

これを使用すればuseStateのisLoadingを使わずに読込中に「loading…」を表示させるコードを書くことができます。

TSX
return (
  <>
    <div>
      <button onClick={onClickHandler}>データを取得する</button>
    </div>
    <Suspense fallback={<p>loading...</p>}>
      <SuspendProducts />
    </Suspense>
  </>
)

Suspenseを使用したコードに変更する方法

まず、Suspenseを使用していない以下のようなコードがあったとします。

TSX
import { useState } from 'react'

type Product = {
  id: number
  title: string
}

function App() {
  const [products, setProducts] = useState<Product[]>([])
  const [isLoading, setIsLoading] = useState(false)
  const [isClicked, setIsClicked] = useState(false)
  const [isError, setIsError] = useState(false)

  const onClickHandler = async() => {
    if (isClicked) return
    setIsLoading(true)
    setIsClicked(true)
    setIsError(false)

    try {
      await new Promise(resolve => setTimeout(resolve, 1000))
      const res = await fetch('https://dummyjson.com/products')
      if (!res.ok) setIsError(true)
      const data = await res.json()
      setProducts(data.products)
    } catch (error) {
      setIsError(true)
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <>
      <div>
        <button onClick={onClickHandler}>データを取得する</button>
      </div>
      <div>
        {isLoading ? (
          <p>loading...</p>
        ) : (
          products.map((product) => (
            <p key={product.id}>{product.id} {product.title}</p>
          ))
        )}
        {isError && <p style={{ color: 'red' }}>エラーが発生しました</p>}
      </div>
    </>
  )
}

export default App

Reactでfetchで読み込み時にloading…を表示したサンプル(Suspenseなし)

Suspenseを使用したコードへの修正手順

1. reactからSuspenseをimportします。

TSX
import { useState, Suspense } from 'react'

2. 「const [isLoading, setIsLoading] = useState(false)」は使わないので削除します。

3. onClickHandlerをsetIsClicked(true)のフラグ切り替えだけの処理にします。

TSX
const onClickHandler = () => setIsClicked(true)

4. ProductsResponseの型を新たに作成します。

TSX
type ProductsResponse = {
  products: Product[]
}

5. fetchでデータを取得するためのgetProductsData関数とコンポーネント用のSuspendProducts関数を作成します。

TSX
const getProductsData = async(): Promise<ProductsResponse> => {
  await new Promise(resolve => setTimeout(resolve, 1000))
  const res = await fetch('https://dummyjson.com/products')
  return res.ok ? res.json() : setIsError(true)
}

const SuspendProducts = () => {
  if (isError) {
    return <p style={{ color: 'red' }}>エラーが発生しました</p>
  }

  if (isClicked && products.length === 0) {
    throw getProductsData()
      .then((data) => setProducts(data.products))
      .catch(() => setIsError(true))
  }

  return (
    products.map((product) => (
      <p key={product.id}>{product.id} {product.title}</p>
    ))
  )
}

6. Appのreturn内のコードをSuspenseを使用したコードに変えます。

TSX
return (
  <>
    <div>
      <button onClick={onClickHandler}>データを取得する</button>
    </div>
    <Suspense fallback={<p>loading...</p>}>
      <SuspendProducts />
    </Suspense>
  </>
)

以上の手順が完了すれば、以下のようなコードとなり、読込中はSuspenseのfallbackで指定した「loading...」が表示されて、fetchでの読み込み後にJSONの内容(SuspendProducts)が表示されます。

修正後の完成したコードは以下のとおりです。

TSX
import { useState, Suspense } from 'react'

type Product = {
  id: number
  title: string
}

type ProductsResponse = {
  products: Product[]
}

const App = () => {
  const [products, setProducts] = useState<Product[]>([])
  const [isClicked, setIsClicked] = useState(false)
  const [isError, setIsError] = useState(false)

  const onClickHandler = () => setIsClicked(true)

  const getProductsData = async(): Promise<ProductsResponse> => {
    await new Promise(resolve => setTimeout(resolve, 1000))
    const res = await fetch('https://dummyjson.com/products')
    return res.ok ? res.json() : setIsError(true)
  }

  const SuspendProducts = () => {
    if (isError) {
      return <p style={{ color: 'red' }}>エラーが発生しました</p>
    }

    if (isClicked && products.length === 0) {
      throw getProductsData()
        .then((data) => setProducts(data.products))
        .catch(() => setIsError(true))
    }

    return (
      products.map((product) => (
        <p key={product.id}>{product.id} {product.title}</p>
      ))
    )
  }

  return (
    <>
      <div>
        <button onClick={onClickHandler}>データを取得する</button>
      </div>
      <Suspense fallback={<p>loading...</p>}>
        <SuspendProducts />
      </Suspense>
    </>
  )
}

export default App

だいぶ大雑把に説明しましたが、実際にコードを書いて動かしたほうがわかりやすいと思ったので、細かい説明は割愛しました。

Suspense版のサンプルも用意しましたので、実際に実行して確かめてみてください。

Reactでfetchで読み込み時にloading…を表示したサンプル(Suspenseあり)