2025年にNext.jsアプリケーションにtRPC + TanStack Queryを導入した話
IT技術
はじめに
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点を重視しました:
- 導入の容易さ: 既存のNext.jsプロジェクトに最小限のコストで導入できること
- TypeScript特化: サーバークライアント両方TypeScriptであり、汎用的なrpcやGraphQLなどは不要
- コミュニティの規模: ドキュメントが充実しており、開発が活発で、トラブルシューティングが容易であること
検討した選択肢
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は queryKey と queryFn が独立した設計になっています。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
ここでハマりやすいのが、「infiniteQueryOptions は cursor を受け取る 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});クライアント側は useInfiniteQuery に infiniteQueryOptions を渡し、getNextPageParam で nextCursor を返します。
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
もしよければ参考にしてください。
参考文献
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!カジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ

Webアプリケーションの開発をしています。




