useStateのloading処理はuseTransitionに変えたほうが良い

useStateを使用したloading表示

Reactでfetch APIなどを使って、指定されたURLに基づいて情報を取得する非同期処理を行うことがあります。

その場合、データを取得するまで時間がかかるため、以下のようにデータを取得するまでuseStateで状態を管理して「Loading...」のようなを表示させることが多いです。

App.tsx
import { useState } from 'react'
import './App.css'

type User = {
  id: number
  firstName: string
}

async function getUser(userId: string): Promise<User | null> {
  try {
    const response = await fetch(`https://dummyjson.com/users/${userId}`)
    if (!response.ok) {
      throw new Error('ユーザーが見つかりません')
    }
    const data: User = await response.json()
    return data
  } catch (error) {
    console.error(error)
    return null
  }
}

function App() {
  const [userId, setUserId] = useState<string>('1')
  const [user, setUser] = useState<User | null>(null)
  const [isLoading, setIsLoading] = useState<boolean>(false)

  const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault()
    if (!userId) return
    setUser(null)
    setIsLoading(true)
    const fetchedUser = await getUser(userId)
    setIsLoading(false)
    setUser(fetchedUser)
  }

  return (
    <>
      <form>
        <input
          type='number'
          onChange={(e) => setUserId(e.target.value)}
          value={userId}
        />
         
        <button onClick={handleClick}>search</button>
      </form>
      {isLoading && <p>Loading...</p>}
      {user && (
        <div>
          <p>id: {user.id}</p>
          <p>firstName: {user.firstName}</p>
        </div>
      )}
    </>
  )
}

export default App

ReactのuseStateで「Loading…」を表示するサンプル

useTransitionを使用したloading表示

useTransitionとはUIをブロックすることなく状態を更新できるReact Hookです。

前述のコードではuseStateを利用して、setIsLoadingにboolean値を設定して「Loading...」の表示・非表示を切り替えていました。

App.tsx
const [isLoading, setIsLoading] = useState<boolean>(false)

const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
  e.preventDefault()
  if (!userId) return
  setUser(null)
  setIsLoading(true)
  const fetchedUser = await getUser(userId)
  setIsLoading(false)
  setUser(fetchedUser)
}

useTransitionを使用すればsetIsLoadingでtrue, falseを設定せずにisLoadingの状態を変更できるので、コードがシンプルになります。

setIsLoading(true), setIsLoading(false)だと記述する場所を間違えて正しく動作しないケースがあるので、useTransitionを使用したほうがバグも発生しづらいです。

TSX
import React, { useState, useTransition } from 'react'
import './App.css'

type User = {
  id: number
  firstName: string
}

async function getUser(userId: string): Promise<User | null> {
  try {
    const response = await fetch(`https://dummyjson.com/users/${userId}`)
    if (!response.ok) {
      throw new Error('ユーザーが見つかりません')
    }
    const data: User = await response.json()
    return data
  } catch (error) {
    console.error(error)
    return null
  }
}

function App() {
  const [userId, setUserId] = useState<string>('1')
  const [user, setUser] = useState<User | null>(null)
  const [isLoading, startTransition] = useTransition()

  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault()
    if (!userId) return
    setUser(null)
    startTransition(async () => {
      const fetchedUser = await getUser(userId)
      setUser(fetchedUser)
    })
  }

  return (
    <>
      <form>
        <input
          type="number"
          onChange={(e) => setUserId(e.target.value)}
          value={userId}
        />
         
        <button onClick={handleClick}>search</button>
      </form>
      {isLoading && <p>Loading...</p>}
      {user && (
        <div>
          <p>id: {user.id}</p>
          <p>firstName: {user.firstName}</p>
        </div>
      )}
    </>
  )
}

export default App

ReactのuseTransitionで「Loading…」を表示するサンプル

※ 2024年9月現在、React v19を使用するには「npm i react@next react-dom@next」でインストールが必要です。

※ useTransitionで非同期関数が使えるのはv19以降なので、動作しなければ{React.version}でバージョンが19以上か確認してください。

※ 2024年9月現在は「Argument of type '() => Promise' is not assignable to parameter of type 'TransitionFunction'.」の警告が出てしまいます。

※ useTransitionを使用する方法だと、データをlocalStorageで保存して、すでに取得したデータの場合はローカルに保存したデータを返す処理を使用している場合は、「Loading...」が一瞬表示されてしまう問題が発生します。useStateだとこの問題は発生しない。