Next.js(App Router利用)でtRPCを動かす

投稿日
Next.js(App Router利用)でtRPCを動かす

フロント/バックエンド側もNext.jsを用いつつtRPCを導入する方法をご紹介します!


またNext.js 13.4 より安定版が使えるようになったApp Routerを用いて実装していきます。

検証した環境

1 node.js 18.15.0
2 next 13.4.10
3 @trpc/client 10.36.0
4 @trpc/next 10.36.0
5 @trpc/react-query 10.36.0
6 @trpc/server 10.36.0
7 @tanstack/react-query 4.32.0

必要なパッケージを追加

Next.jsがApp Routerで動く前提で記載していきます


Next.jsのApp Routerに導入する上で以下のリポジトリが大変参考になりましたmm

devietti/trpc-next13-app: tRPC + Next 13 app directory + TypeScript



公式に書かれているパッケージを導入していきます

$ yarn add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod

バックエンド側を用意

ここで用意するファイルは2つです

  1. trpc-server.ts・・実際の処理を記載する
  2. route.ts・・Next.js Route Handler機能を用いてtRPCを実行出来るようにする

trpc-server.ts

まず、tRPC内で行う実際の処理を記載します



参考用として

  • アクセスすると「Hello World」というテキストを返す helloWorld
  • ユーザーIDを受け取って該当のユーザーを返す userById

の2つを定義しています

src/libs/trpc-server.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
 
const t = initTRPC.create()
 
interface User {
  id: string
  name: string
}
// 参考用のユーザーリスト
const userList: User[] = [
  { id: '1', name: 'Hoge' },
  { id: '2', name: 'Fuga' },
]
 
export const appRouter = t.router({
  helloWorld: t.procedure.query(() => {
    return 'Hello World'
  }),
  userById: t.procedure
    .input(z.number() /* userById を呼び出す時数値(userId)が必要とする */)
    .query((request) => {
      const { input } = request
      // inputの値を元にユーザーリストから該当するユーザーを返す
      return userList.find((u) => Number.parseInt(u.id) === input)
    }),
})
 
// 最終的にフロントでも用いるためexportしておく
export type AppRouter = typeof appRouter

余談として、tRPCの↑の処理部分を appRouter という名前で定義するのが定石のようです。

Next.jsの App Router と名前がバッティングして検索がしづらいという…w 😂


Next.js Route Handler

Next.jsの Route Handler機能を使ってtRPCを呼び出せるようにします


Next.jsのApp RouterになってRoute Handlerの書き方が変わりました。

Pages Router・App Router どちらを使っているかで以下の記述方法も変わります

src/app/api/trpc/[trpc]/route.ts
import { FetchCreateContextFnOptions, fetchRequestHandler } from '@trpc/server/adapters/fetch'
 
import { appRouter } from '@/libs/trpc-server'
 
const handler = (request: Request) => {
  console.log(`incoming request ${request.url}`)
  return fetchRequestHandler({
    endpoint: '/api/trpc',
    req: request,
    router: appRouter,
    // eslint-disable-next-line unused-imports/no-unused-vars
    createContext(options: FetchCreateContextFnOptions): object | Promise<object> {
      return {}
    },
  })
}
 
export const GET = handler
export const POST = handler

↑へのGET・POSTのアクセス (localhost:3000/api/trpc/${何かしらの値} ) は全て

先ほど記述したAppRouterに流す、という処理のようですね!


tRPCにアクセス出来る事を確認

ローカル環境を起動し以下のURLにアクセスします


http://localhost:3000/api/trpc/helloWorld

tRPCに定義したhelloWorldにアクセス出来た


http://localhost:3000/api/trpc/userById?input=2

tRPCに定義したuserByIdにアクセス出来た

どちらも無事アクセス出来る事が確認出来ました! 🎉


フォルダ構成

ここまででフォルダ構成は以下のようになっています

$ tree -L 6
.
├── README.md
├── src
│   ├── app
│   │   ├── api
│   │   │   └── trpc
│   │   │       └── [trpc]
│   │   │           └── route.ts
・・・
│   ├── libs
│   │   └── trpc-server.ts
・・・

フロントからtRPCを呼び出す

いよいよフロントから呼び出します!



追加を2点

  1. trpc.ts・・フロントでカスタムフックとして使えるようにする
  2. TrpcProvider.tsx・・フロント全体でtRPCを使えるようにする

変更を1点

  1. layout.tsxでTrpcProvider.tsxをラップする

です

trpc.ts

libsにReactで使える trpc という変数をexportする、trpc.tsを追加します

src/libs/trpc.ts
import { createTRPCReact } from '@trpc/react-query'
 
import type { AppRouter } from './trpc-server'
 
export const trpc = createTRPCReact<AppRouter>()

TrpcProvider.tsx

tRPC用のProviderを追加します


公式に詳細が記載されていないため把握し切れていませんが、

tRPCで取得した値の共有・キャッシュの制御なんかをやっているのかな、という予想です

src/libs/TrpcProvider.tsx
'use client'
 
import { FC, PropsWithChildren, useState } from 'react'
 
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink } from '@trpc/client'
 
import { trpc } from './trpc'
 
export const TrpcProvider: FC<PropsWithChildren> = ({ children }) => {
  const [queryClient] = useState(() => new QueryClient())
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [httpBatchLink({ url: '/api/trpc' })],
    }),
  )
 
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  )
}

layout.tsx内でラップする

↑で作成したProviderでラップします!


私は今回アプリケーション全体でtRPCを使用するため、

1番トップレベルのlayout.tsxでラップしています。


例えばもっと限定的な場合はその部分のコンポーネントのみでラップすればいいかなと思います。

src/app/layout.tsx
import './globals.css'
import { ReactNode } from 'react'
 
import { TrpcProvider } from '@/libs/TrpcProvider'
 
const RootLayout = ({ children }: { children: ReactNode }) => {
  return (
    <TrpcProvider>
      <html lang="ja">
        <body>{children}</body>
      </html>
    </TrpcProvider>
  )
}
 
export default RootLayout

コンポーネントで使用する

お待たせしました!


いよいよ、コンポーネントで使用してみます

src/app/sample/page.tsx
'use client'
 
import { NextPage } from 'next'
 
import { trpc } from '@/libs/trpc'
 
const SamplePage: NextPage = () => {
  const user = trpc.userById.useQuery(1)
 
  return (
    <main>
      <p>hello, {user.data?.name}</p>
    </main>
  )
}
 
export default SamplePage


ローカルホストを起動し該当ページにアクセスすると

フロントからもtRPCにアクセス出来た

無事表示されました!🎉




分かってくるとここまでの実装もサクッと出来て、

コード補完もしっかり効くのでDXかなりいいです!☺️



クライアント側で呼び出しているので "use client" を記述してますが、

Next.js App RouterのうまみでもあるSSRと連携して使えると更に良さそうですねー!



フォルダ構成

最後にここまでの主要ファイルのフォルダ構成です

$ tree -L 6
.
├── README.md
├── src
│   ├── app
│   │   ├── api
│   │   │   └── trpc
│   │   │       └── [trpc]
│   │   │           └── route.ts
│   │   ├── layout.tsx
│   │   └── sample
│   │       └── page.tsx
│   ├── libs
│   │   ├── TrpcProvider.tsx
│   │   ├── trpc-server.ts
│   │   └── trpc.ts


プロフィール画像
Yuki Takara
都内でフリーランスのエンジニアをやってます。フロントとアプリ開発メインに幅広くやってます。