• トップ
  • ブログ一覧
  • 2025年にNext.jsアプリケーションにtRPC + TanStack Queryを導入した話
  • 2025年にNext.jsアプリケーションにtRPC + TanStack Queryを導入した話

    はじめに

    Next.js App Routerで React Server Componentや Server Functionが導入されましたが、クライアントサイドからデータを取得したい場面ではあまり向いておらず、従来のAjaxで対応した方が望ましいケースがあります。

    担当しているNext.jsプロジェクトでは、そのようなケースを以下のような構成で実装していました:

    • API Routesによる REST API 作成
    • クライアントコンポーネントにてuseEffect + axiosによるデータ取得

    この構成にはいくつかの課題がありました:

    従来の実装における課題

    • 型安全性の欠如: フロントエンドとバックエンドで型定義を手動で同期する必要がある
    • ボイラープレートコードの増加: 各API Route実装、axiosによる呼び出し関数の実装など手間がかかる
    • useEffectの落とし穴: race condition未対応, cleanup関数不在などの実装の漏れが多数

    これらの課題を解決するため、tRPC + TanStack Queryを導入し、いくつかのユースケースに適用しました。本記事では、ライブラリ選定の経緯と具体的な例を紹介します。

    ライブラリ選定の背景

    今回の技術選定では、以下の3点を重視しました:

    1. 導入の容易さ: 既存のNext.jsプロジェクトに最小限のコストで導入できること
    2. TypeScript特化: サーバークライアント両方TypeScriptであり、汎用的なrpcやGraphQLなどは不要
    3. コミュニティの規模: ドキュメントが充実しており、開発が活発で、トラブルシューティングが容易であること

    検討した選択肢

    RPC層

    TypeScript環境での型安全な通信を実現するため、以下のライブラリを比較検討しました:

    • tRPC: この3つの中でダウンロード数が最も多く、T3 Stackを参考に導入しやすいそう
    • @effect/rpc: 関数型プログラミングのパラダイムを取り入れており強力そうだが、導入のハードルが高そう
    • oRPC: 新興ライブラリで利用者数はまだ少ないが、tRPCとAPI設計が似ているため、まずはtRPCを採用し、移行検討でも良さそう

    データフェッチング層

    TanStack Query

    Webアプリケーションでサーバー状態を取得、キャッシュ、同期、更新するライブラリです。この系のライブラリでは最も人気のある1つで、以下のような感じで使用します。

    1const { data } = useQuery({
    2  queryKey: ['post', postId],
    3  queryFn: () => fetch(`/api/post?id=${postId}`).then((res) => res.json()),
    4  enabled: shouldFetch,
    5  refetchInterval: 10_000,
    6})

    tRPCの公式integrationでは以下のように使用します. oRPCにも類似なintegrationが用意されています.

    1const { data } = useQuery(
    2  trpc.post.get.queryOptions({
    3    id: postId
    4  }, {
    5    enabled: shouldFetch,
    6    refetchInterval: 10_000,
    7  })
    8)
    SWR

    vercelが開発しているデータ取得ライブラリです。以下のような感じで使用します。

    1const { data } = useSWR(
    2  shouldFetch ? `/api/post?id=${postId}` : null,
    3  (url) => fetch(url).then((res) => res.json()),
    4  { refreshInterval: 10_000 },
    5)

    特徴として、useSWR の第2引数 fetcher は第1引数 key を受け取ってデータを取得する非同期関数です。RESTful APIやGraphQLなどのAPIサーバーと通信する場合は、fetcherを共通化することで簡潔に記述できます。しかし、tRPCなどのRPC層と組み合わせる場合、key の管理において型安全性を諦めて単なる文字列にするか、独自のレイヤーを構築するか、あるいはサードパーティ製のライブラリを導入する必要があると感じました。

    一方、TanStack Queryは queryKeyqueryFn が独立した設計になっています。RESTful APIなどでは情報の重複が冗長な気もしますが、tRPCやoRPCの統合環境ではキー管理が自動化されるため、その点は問題になりません。

    選定

    最終的には、tRPC + TanStack Query の組み合わせに決めました。

    tRPCのエコシステムには「T3 Stack」という有名なテンプレートがあり、Next.js + tRPC + TanStack Query を組み合わせた構成をサクッと生成できます。tRPCのコアメンバーである Julius Marminge 氏が T3 Stack のメインテナーでもあることから、公式に近い「動く事例」を参考にできるのは大きな魅力です。断片的なコード例に頼るよりも、手軽に導入できると判断しました。

    ハマったところ

    T3 Stack では現状 tRPC の最新の TanStack Query Integration が採用されていない

    2025年2月、tRPC は新しい TanStack Query Integration を公開しました。https://trpc.io/blog/introducing-tanstack-react-query-client

    1// 新しい Integration: TanStack Query の標準的な hooks を使用
    2import { useQuery } from '@tanstack/react-query'
    3import { useTRPC } from "~/trpc/client"
    4
    5export const Post = () => {
    6  const trpc = useTRPC()
    7  const { data } = useQuery(
    8    trpc.post.get.queryOptions({
    9      id: postId
    10    })
    11  )
    12}

    対して、従来の Integration は tRPC が TanStack Query をラップした独自の hooks を提供する形式でした。

    1// 従来の Integration: tRPC 独自の hooks を使用
    2import { trpc } from "~/trpc/client"
    3
    4export const Post = () => {
    5  const { data } = trpc.post.get.useQuery({
    6    id: postId
    7  })
    8}

    この新しい Integration(queryOptions を使う形式)には、以下のようなメリットがあります:

    • 境界の明確化: useQuery(trpc.post.get.queryOptions(...)) と書くことで、コード上で TanStack Query と tRPC の境界が明確になり、可読性が向上します。
    • 公式ドキュメントとの親和性: TanStack Query の hooks をそのまま利用するため、公式ドキュメントの知識を直接活かすことができ、学習コストが下がります。
    • 疎結合: tRPC を使わなくなった場合でも、TanStack Query 側のロジックはそのまま使用できます。

    しかし、2026年1月現在、T3 Stack (create-t3-app) ではこの新しい Integration は採用されていません。https://github.com/t3-oss/create-t3-app/issues/2065

    公式に近い「動く事例」として期待していましたが、最新の Integration を取り入れるには、改めて公式ドキュメントを確認しながら進める必要がありました。

    infiniteQueryOptions を使うには tRPC 側で cursor を受け取る必要がある

    無限スクロールを実装する際は、TanStack Query の useInfiniteQuery(または useSuspenseInfiniteQuery)と、tRPC の infiniteQueryOptions を組み合わせるのが手軽です。

    Available for all query procedures that takes a cursor input.
    https://trpc.io/docs/client/tanstack-react-query/usage#infiniteQueryOptions

    ここでハマりやすいのが、「infiniteQueryOptionscursor を受け取る query procedure に対してのみ提供される」という点です。つまり、該当の procedure の input に cursor フィールドが存在しないと、クライアント側で trpc.post.getPosts.infiniteQueryOptions(...) が生えません。

    サーバー側(tRPC ルーター)では、input に cursor を含め、レスポンスに nextCursor を含める形にしておくと、getNextPageParam と自然に接続できます。

    1// src/server/api/routers/post.ts
    2export const postRouter = createTRPCRouter({
    3  getPosts: publicProcedure
    4    .input(
    5      z.object({
    6        // ...
    7        cursor: z.number().nullish(), // infiniteQueryOptions を使用するためには cursor フィールドが必要(型は問わない)
    8      }),
    9    )
    10    .query(async ({ input }) => {
    11      const { cursor } = input;
    12      // ... cursor を基準にデータを取得
    13      return {
    14        // ...
    15        nextCursor, // クライアント側 getNextPageParam が返す値
    16      };
    17    }),
    18});

    クライアント側は useInfiniteQueryinfiniteQueryOptions を渡し、getNextPageParamnextCursor を返します。

    1"use client";
    2
    3import { useInfiniteQuery } from "@tanstack/react-query";
    4import { useTRPC } from "~/trpc/client";
    5
    6export function Posts() {
    7  const trpc = useTRPC();
    8
    9  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    10    useInfiniteQuery(
    11      trpc.post.getPosts.infiniteQueryOptions(
    12        {
    13          // trpc.post.getPosts の input
    14        },
    15        {
    16          getNextPageParam: (lastPage) => lastPage.nextCursor, // useInfiniteQuery の必須オプション
    17          // 他の useInfiniteQuery オプション
    18        },
    19      ),
    20    );
    21
    22  // ...
    23}

    公式ドキュメントにはさらっと書かれているものの、コード例では cursor の必須性が明示されていないため、なぜ infiniteQueryOptions を呼べないのかに気づくまで少し時間がかかりました。旧 Integration 側の useInfiniteQuery の説明を見ると、この前提が分かりやすいです。

    https://trpc.io/docs/client/react/useInfiniteQuery

    最後に

    tRPC + TanStack Query を導入したことで、型安全性の向上とボイラープレートコードの大幅な削減を実現できました。特に新しい Integration では、TanStack Query と tRPCの区切りがはっきりしているため、TanStack Query の 知識が活用しやすく、tRPCをやめることになっても安心感があります。

    Next.js などのRPCの提供がないフレームワークでクライアントサイドのデータフェッチングが必要な場面では、この構成が有力な選択肢の一つになると考えています。

    ただ、最新のApp Router, Integrationのサンプルアプリなどはあまりなく、知名度割には簡単に導入はできなかったです。本記事で紹介した実装の完全なコードは、以下のGitHubリポジトリで公開しています:

    https://github.com/hynjnk/tanstack-query-trpc-infinite-scroll-demo

    もしよければ参考にしてください。

    参考文献

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!カジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    キムくん(エンジニア)
    キムくん(エンジニア)
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background