🗂

tRPCで知っておくべき3つのデータ操作パターン:useQuery、useMutation、utils.fetchの使い分け完全ガイド

に公開

tRPCで混乱しがちな3つのデータ操作パターンを完全解説

tRPCを使い始めたとき、「データ取得や更新の方法が複数あるけど、どれを使えばいいの?」と迷った経験はありませんか?

実際、tRPCには主に3つのパターンがあります:

  • useQuery - 自動的なデータ取得
  • useMutation - データ変更操作
  • utils.fetch - 手動でのAPI呼び出し

この記事では、それぞれの特徴と適切な使い分け方を、実際のコード例とともに詳しく解説します。

まず理解しておきたい3つのパターン

パターン 用途 実行タイミング 状態管理
useQuery データ表示 自動(マウント時) 自動
useMutation データ変更 手動(イベント時) 自動
utils.fetch 一回限り処理 手動(イベント時) 手動

useQuery:画面表示用データの自動取得

特徴

  • コンポーネントマウント時に自動実行
  • キャッシュ機能で高速化
  • ローディング・エラー状態を自動管理

実用例:ユーザー一覧の表示

const UserListPage = () => {
  const { 
    data: users, 
    isLoading, 
    error 
  } = trpc.user.getAll.useQuery()

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  
  return (
    <div>
      {users?.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  )
}

いつ使う?

  • ページ読み込み時に必要なデータ
  • リアルタイムで更新が必要なデータ
  • 複数の場所で使い回すデータ

useMutation:データ変更操作の状態管理

特徴

  • ボタンクリックなどのイベントで実行
  • 成功・失敗のコールバック処理
  • ローディング状態の自動管理

実用例:ユーザー作成フォーム

const CreateUserForm = () => {
  const utils = trpc.useUtils()
  
  const createUserMutation = trpc.user.create.useMutation({
    onSuccess: () => {
      toast.success('ユーザーが作成されました')
      // キャッシュを更新して一覧に反映
      utils.user.getAll.invalidate()
    },
    onError: (error) => {
      toast.error(`エラー: ${error.message}`)
    }
  })

  const handleSubmit = (formData) => {
    createUserMutation.mutate(formData)
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* フォーム要素 */}
      <button 
        type="submit" 
        disabled={createUserMutation.isLoading}
      >
        {createUserMutation.isLoading ? '作成中...' : '作成'}
      </button>
    </form>
  )
}

いつ使う?

  • フォーム送信
  • データの作成・更新・削除
  • 成功時にUIを更新したい操作

utils.fetch:一回限りの手動API呼び出し

特徴

  • 必要な時だけ実行
  • Promise形式で結果を取得
  • 状態管理は自分で行う

実用例:ファイルダウンロード

const FileDownloadButton = ({ fileId }) => {
  const [downloading, setDownloading] = useState(false)
  const utils = trpc.useUtils()

  const handleDownload = async () => {
    try {
      setDownloading(true)
      
      const data = await utils.file.getDownloadUrl.fetch({ 
        fileId 
      })
      
      // ダウンロード実行
      const link = document.createElement('a')
      link.href = data.url
      link.download = data.fileName
      link.click()
      
      toast.success('ダウンロードを開始しました')
    } catch (error) {
      toast.error('ダウンロードに失敗しました')
    } finally {
      setDownloading(false)
    }
  }

  return (
    <button 
      onClick={handleDownload}
      disabled={downloading}
    >
      {downloading ? 'ダウンロード中...' : 'ダウンロード'}
    </button>
  )
}

いつ使う?

  • ダウンロード処理
  • 条件付きのデータ取得
  • 複雑なエラーハンドリングが必要な場合

実践的な使い分けフローチャート

よくある間違いパターンと修正例

❌ 間違い:useQueryでデータ変更

// これはダメ
const { data } = trpc.user.delete.useQuery({ id: userId })

✅ 正解:useMutationでデータ変更

const deleteUserMutation = trpc.user.delete.useMutation({
  onSuccess: () => utils.user.getAll.invalidate()
})

まとめ:適切なパターンの選び方

  1. 画面に表示するデータuseQuery

    • 自動取得、キャッシュ、リアルタイム更新
  2. ユーザーアクションでデータ変更useMutation

    • フォーム送信、CRUD操作、状態管理
  3. 特定条件での一回限り処理utils.fetch

    • ダウンロード、条件付き取得、カスタムエラーハンドリング

この使い分けを意識することで、tRPCの恩恵を最大限活用した、保守性の高いReactアプリケーションを構築できます。

Discussion