フロント/バックエンド側も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つです
- trpc-server.ts・・実際の処理を記載する
- route.ts・・Next.js Route Handler機能を用いてtRPCを実行出来るようにする
trpc-server.ts
まず、tRPC内で行う実際の処理を記載します
参考用として
- アクセスすると「Hello World」というテキストを返す
helloWorld
- ユーザーIDを受け取って該当のユーザーを返す
userById
の2つを定義しています
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 どちらを使っているかで以下の記述方法も変わります
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
http://localhost:3000/api/trpc/userById?input=2
どちらも無事アクセス出来る事が確認出来ました! 🎉
フォルダ構成
ここまででフォルダ構成は以下のようになっています
$ tree -L 6
.
├── README.md
├── src
│ ├── app
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [trpc]
│ │ │ └── route.ts
・・・
│ ├── libs
│ │ └── trpc-server.ts
・・・
フロントからtRPCを呼び出す
いよいよフロントから呼び出します!
追加を2点
- trpc.ts・・フロントでカスタムフックとして使えるようにする
- TrpcProvider.tsx・・フロント全体でtRPCを使えるようにする
変更を1点
- layout.tsxでTrpcProvider.tsxをラップする
です
trpc.ts
libsにReactで使える trpc
という変数をexportする、trpc.tsを追加します
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from './trpc-server'
export const trpc = createTRPCReact<AppRouter>()
TrpcProvider.tsx
tRPC用のProviderを追加します
公式に詳細が記載されていないため把握し切れていませんが、
tRPCで取得した値の共有・キャッシュの制御なんかをやっているのかな、という予想です
'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でラップしています。
例えばもっと限定的な場合はその部分のコンポーネントのみでラップすればいいかなと思います。
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
コンポーネントで使用する
お待たせしました!
いよいよ、コンポーネントで使用してみます
'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
ローカルホストを起動し該当ページにアクセスすると
無事表示されました!🎉
分かってくるとここまでの実装もサクッと出来て、
コード補完もしっかり効くので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