注釈だらけのtRPCとNext.js ~Helloをもらうまで~(1)
Build a Blog With the T3 Stack - tRPC, TypeScript, Next.js, Prisma & Zod
こちらの動画と一緒に進めていく。
動画を見ながら、気になった点について全て調べていく。
1-1. のように番号ハイフン番号となっているのは、1の操作に対する補足情報である。1には行う操作(コードを書く、コマンドを打つ)ことのみを書いている。
0: package.jsonを丸写ししてyarn install する。
全く同じ環境を再現したい方向け。とりあえずエラーの可能性を低くして作りきり全体を理解してから必要なアップデートを行う方針にする。
講師のGitHub
https://github.com/TomDoesTech/trpc-tutorial
- tRPCのReact Queryフックを作成する
//src/utils/trpc.ts
import {createReactQueryHooks} from '@trpc/react'
///todo add appRouter as generic
export const trpc = createReactQueryHooks()
1-1 tRPCとは、APIのエンドポイントを静的に型づけられるライブラリである
TypeScriptとの相性がよく、フロントエンドとバックエンドのやりとりにおけるデータの不整合を防いでくれる。
1-2 React QueryはReactが非同期でデータを取得するのに使うライブラリである。
サーバから取得したデータがキャッシュされ、データを再び取得する・更新する・データを同期するときの管理を効率化してくれる。
createReactQueryHooksはtrpcのエンドポイントを、ReactQueryHooksに変換する
するとReactのコンポーネントで
tRPCのエンドポイントを使えるだけでなく、React Queryの機能(データのキャッシュなど)が加わった状態で扱える。
1-3 あとでやることとして”todo add appRouter as generic”と書いている
ジェネリクスはTypeScriptの機能であり
型をパラメタとして関数・クラスに渡すことができるというもの。
関数・クラスが任意の型を受け取り、型が再利用でき柔軟性を与えられたものになる
ジェネリックとしてappRouterを加えると、以下のものの型が正しく推測される
エンドポイントの引数, フックの返すデータ, フックの返すエラー
- _app.tsxの下部でwithTRPCをエクスポートする
//src/pages/api/_app.tsx
//一番下部の、export default Appを消す。
export default withTRPC({
})
2-1 ここではtRPCのライブラリが使用してReactのコンポーネントをReactのHOCでラップする。
具体的には、withTRPC関数がtRPCの機能を持ったHOCを作る。
HOC(高階のコンポーネント)は、他のコンポーネントを引数としてとる。
HOCはコンポーネントを再利用したり、ロジックを抽出したり、状態を抽出したりとさまざまな使い方がある。
- withTRPC関数に設定のオブジェクトを渡す。
//todo add appRouter to generic
export default withTRPC({
config({ctx}) {
const url = process.env.NEXT_PUBLIC_VERCEL_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL/api/trpc}`
return url
}
})
3-1 withTPCの中の関数、configについて
withTRPCには次のような情報が設定されることが多い。
config… ctxオブジェクトを受け取る。tRPCエンドポイントへのリクエストに関する設定をする。
configが返すのは、http(HTTPリクエストのための設定), ctx(tRPCエンドポイントのコンテクスト)などがある。
3-2 本番環境と開発環境でのURLを用意
process.env.NEXT_PUBLIC_VERCEL_URLが存在する ⇒ APIのURLはそれを使用して作られる
存在しない ⇒ localhost:3000/api/trpcを使う。
4.Youtubeのpackage.jsonをコピペする。
4-1 yarn installする
5.trpcのclientからいくつかのものをインポートする
import { loggerLink } from '@trpc/client/links/loggerLink'
import { httpBatchLink } from '@trpc/client/links/httpBatchLink'
5-1 注意点: package.jsonの指定 tRPCはv9とv10で変更点がある
"@trpc/client": "^9.25.3",
今回はversionはこちらを使用する(完全に動画と合わせている)
5-2 httpBatchLinkの役割
httpBatchLinkはHTTPリクエストをバッチ化する機能を提供している。
バッチ化は、複数のリクエストを一つにまとめてネットワークの帯域を圧迫しないようにする。
5-3 loggerLinkの役割
全てのリクエスト・レスポンスをコンソールに出力してくれる。
6 return urlではなくlinksに変更する
const linksとして、loggerLinkとhttpBatchLinkを含む配列を追加。
return するものを追加。
const links = [
loggerLink(),
httpBatchLink({
maxBatchSize: 10,
url,
}),
]
return {
},
links,
}
},
6-1 tRPCにおけるlinkとは?
リクエスト/ レスポンスのライフサイクル全体で、カスタムなロジックを可能にする。
中間層に位置していて、ログやエラーハンドリング、キャッシングを担当する。
6-2 linkの順番
配列内の定義順で実行される。この場合、loggerLink → httpBatchLinkの順なので、
ログはバッチ処理が行われる前のリクエスト・レスポンスを表示する。
7 queryClientConfigの追加
const links = [
// 略
]
return {
queryClientConfig: {
defaultOptions: {
queries:{
staleTime: 60,
},
},
},
links,
}
},
7-1 queryClientConfigの役割
React Queryの設定を定義するオブジェクトである。
その中にあるdefaultOptions.queries.staleTimeというプロパティは
React Queryのクエリデータが更新させるまでに待機する時間を指定する。60秒経過すると自動でReact Queryがデータをリフレッシュするようになっている。
7-2 公式のドキュメント
Initial Query Data | TanStack Query Docs
staleTimeに加え、initialDataというものも解説されている。
8.tRPCのリクエストヘッダーをカスタマイズする
return {
//略
},
headers() {
//ctx
if(ctx?.req) {
return {
...ctx.req.headers,
'x-ssr': '1',
}
}
return {}
},
links,
}
8-1 headers()関数
headers関数が返すオブジェクトは、tRPCのリクエストのヘッダーとして使用される。
8-2 ctx.req
ctxは、Next.jsのページの、コンテキストオブジェクト。
ここには、サーバサイドからのリクエストに関する情報が含まれている。
ctx.reqは、そのリクエストオブジェクトである。
つまり、ここではサーバサイドからリクエストがあった時、
その全てのヘッダー情報を取得し、tRPCのヘッダとして設定している。
8-3’ x-ssr’: ‘1’
リクエストがサーバサイドレンダリングを用いて行われたことを示している。サーバサイドのリクエストをクライアントサイドのリクエストから区別する目的がある。
8-4 return {}
サーバサイドからのリクエストではないとき、ヘッダーを空にするためのコード。
8-5 ctx?.reqについて
オプショナルチェイニングという機能。nullやundefinedである可能性があるオブジェクトの、プロパティにアクセスする時のエラーを防ぐ。もしctxがnull, undefinedなら、エラーを出さずundefinedを返す。存在していればreqを取り出す。
8-6 ctx?.reqについて(2)
if(ctx?.req)はJavaaScriptの機能により、オブジェクトが返ってくるときにはそれがtruthyとみなされる。undefinedはfalsyとみなされる。
9 tRPCのレスポンス・リクエストのデータを文字列にする ↔ 元の形にする
return {
//略
},
headers() {
//略
},
links,
transformer: superjson
}
9-1 transformer: superjsonの追加
superjsonはJavaScriptのデータをシリアライズ(文字列にする)・デシリアライズ(元に戻す)するライブラリである。
普通はJSON.stringifyという関数を使用するが、この関数はDateオブジェクト, undefinedオブジェクト、エラーオブジェクトをシリアライズできない。
superjsonはそのような特殊なデータも適切に文字列にしたり、戻したりできる。
10 全てのクライアントが行うリクエストを見るための設定
export default withTRPC({
config({ctx}) {
//略
},
ssr: false
})
10-1 サーバサイドレンダリングの無効化
サーバサイドレンダリング(ssr)を有効にすると、ページの内容はブラウザではなくサーバサイドで事前にレンダリング可能になる。
講師はこれを、falseに設定する。これは、クライアント(自分のブラウザ)から送られる全てのリクエストを見るためである。
もしこれをtrueに設定するとクライアントがリクエストをサーバ上で行う。するとブラウザの開発者ツールで、networktabを使用して一部のリクエストを見ることは不可能になる。
10-2 ssrオプションの変更
ssrオプションを有効にしなくても十分早くユーザ体験が損なわれない場合にはこれで良いが
もしそうでない場合検討すべき。
11 Reactの最上位コンポーネントをエクスポートする
export default withTRPC({
})(App)
11-1 AppコンポーネントをtRPCのwithTRPCでラップする
AppはReactのアプリケーションのルートである。
AppコンポーネントがwithTRPCでラップされて出力されるので、今後Reactのアプリ全体でtRPCの機能が使えるようになる。
12 いくつかのファイルと、ディレクトリを作成する
- server/route/app.router.ts
server/route/post.router.ts
server/route/user.router.ts
server/createRouter.ts
12-1 tRPCにおけるAPIエンドポイントの定義
tRPCではエンドポイントのルーティングをrouterを使って扱う。
APIのエンドポイントの定義と、それぞれに対する操作を割り当てる。
(GET, POST, DELETE)
app.router.ts, post.router.ts, user.router.tsはそれぞれのAPIエンドポイントを定義するファイルである。
例えばuser.router.tsはユーザの作成をするエンドポイント、取得をするエンドポイントになる
createRouter.tsはそれぞれのAPIエンドポイントをまとめるのに使う。それぞれのエンドポイントにアクセスするためにインターフェースを提供する。
13 tRPCでrouterのインスタンスを作る
// src/server/createRouter.ts
import {router} from '@trpc/server'
import superjson from 'superjson'
export function createRouter() {
// todo add context to generic
return router().transformer(superjson)
}
13-1 routerの作成
trpcからインポートしたrouter関数を使用することでrouterのインスタンスができる
これでエンドポイントの定義および、GET, POST, DELETEを割り当てることが可能になる
13-2 transformerをルータに追加する
.transformerにより、
superjsonをtransformerとして持つルータを作ることができる。
14 APIエンドポイントのリクエスト、レスポンスを含む”コンテクストオブジェクト”の作成
// src/server/createContext.ts
import { NextApiRequest, NextApiResponse } from "next"
export function createContext({
req, res
}:{
req: NextApiRequest,
res: NextApiResponse
}){
return {req, res}
}
export type Context = ReturnType<typeof createContext>
14-1 Next.jsのAPIルーティングにおける型定義
import { NextApiRequest, NextApiResponse } from "next"
でそれぞれ、リクエストとレスポンスの型定義をインポート
14-1-1 型定義の目的
型定義はプログラムの中で使用する変数や関数の引数、戻り値がどのようなデータ形式を持つか明示するために使われる。
let count: number;
count = 10; //正しい
count = "Hello"; //コンパイルエラー
14-2 req, resを受け取る関数の定義
function createContext は 先ほどインポートしたNextApiRequest型, NextApiResponse型のオブジェクトを受け取る。
14-2-1 型の定義
{ req, res }の部分は、引数名を表す。これを:
でつなぐことで引数は以下の型を持つオブジェクトである、という意味になる。
{ req: NextApiRequest, res: NextApiResponse }の部分は型を指定している。
14-2-2 オブジェクトのプロパティを楽に割り当てる
{ req, res }の部分はオブジェクトデストラチャリングという記法である。
オブジェクトから特定のプロパティを取り出した上で、さらにそれらを個別の変数にする。オブジェクトのプロパティには、普通 person.nameなどとしてドットでアクセスするしかない。
// 普通のオブジェクトの書き方
const person = {
name: 'Alice',
age: 25,
};
// personのプロパティはこうアクセスすることしかできないはず
console.log(personOne.name); // Alice
console.log(personOne.age); // 25
// オブジェクトの分解 + 変数への割り当て
const { name, age } = person;
console.log(name); // Alice
console.log(age); // 25
// 変数でアクセスできている。
14-2-3 req, resの実体ができるタイミング
この例ではname ageも実際に存在するけど、
createContextのreq, resもまだ実体がないのでよくわからない
⇒ createContextがどこかで呼び出されたときreq, resが生まれると考える。
14-3 コンテクストオブジェクトの使い方
コンテクストオブジェクトは複数の関連するデータ、機能をオブジェクトにまとめる。
それはアプリケーション全体で共有され、使用される。
Webアプリケーションでユーザの認証情報を管理するような場合、
ログイン状態、ユーザの詳細な情報、権限のデータとログイン・ログアウトという操作を実行する機能が関連する。別々に管理するより、これをユーザコンテクストオブジェクトとする。
今回の場合、req, resをまとめることでリクエスト・レスポンスの一元管理を目指している。
15 createContextの戻り値の方をContextとしてエクスポートする
// createContext.tsの一番した
export type Context = ReturnType<typeof createContext>
15-1 ReturnType
関数型Tの戻り値の型を取得できる
15-2 Contextという新たな型のエクスポート
他のファイルからはcreateContextの関数の戻り値が, Contextとして利用できる。
16 createRouterを使い、新たなルータを作る。appRouterという名前で公開する
// src/server/route/app.router.ts
import { createRouter } from "../createRouter"
export const appRouter = createRouter()
16-1 createRouter関数を呼び出している
おさらいすると、 createRouter 関数の中には一行、
return router<Context>().transformer(superjson)
と書かれているだけである。
これは全ての子ルータで使われる設定なので、切り出している。
17 クエリの追加 + クエリの処理を書く
// さっきのapp.router.ts
import { createRouter } from "../createRouter"
export const appRouter = createRouter()
.query('hello', {
resolve: () => {return 'hello from trpc server'},
})
export type AppRouter = typeof appRouter
17-1 createRouter().query()
helloというクエリを追加しており、その実装が第二引数のオブジェクトにある
クエリが呼ばれたときに’hello from…’という文字列を返すように指定している。
17-2 作成したルータと、その型をそれぞれ公開する(なぜそれぞれ必要なのか?)
export const appRouter ⇒
appRouterは具体的なルータのオブジェクトである。
クエリ、ミューテーション、エンドポイントの定義に使う。
他のファイルにインポートされ、
エンドポイントへのリクエスト処理のロジックを提供する。
export type AppRouter ⇒
AppRouterはappRouterの型情報を表す。
17-3-1 慣例: 変数と関数
変数と関数名にはcamelCaseを使う。最初小文字はじまりでその後の単語の文字を大文字sる
17-3-2 慣例: 型の名前
PascalCaseを使う。各単語の最初の1文字を大文字にする。
18 withTRPC関数が何の型を扱うか指定する
export default withTRPC<AppRouter>({
config({ctx}) {
// 略
},
ssr: false
})(App)
18-1 withTRPC関数が扱うのはAppRouterのみで良い?
user.router.tsやpost.router.tsで今後作成するルータはどうするのか
export const appRouter = createRouter()
.merge(’user.’, userRouter)
.merge(’post.’, postRouter)
.merge('hello', {
resolve: () => {return 'hello from trpc server'}
});
このようにして統合する手段はある。これは、
user.というプレフィックスを持つ場合はuserRouterに処理させ、
post.の場合はpostRouterに処理させるというコードのサンプルである。
19 createReactQueryHooksにも同じ型情報を扱わせておく。
export const trpc = createReactQueryHooks<AppRouter>()
19-1 trpcが持つ情報
const trpc: {
Provider: (props: {
queryClient: QueryClient;
client: TRPCClient<Router<{
req: NextApiRequest;
res: NextApiResponse;
}, {
req: NextApiRequest;
res: NextApiResponse;
}, ... 4 more ..., DefaultErrorShape>>;
children: ReactNode;
isPrepass?: boolean | undefined;
ssrContext?: unknown;
ssrState?: SSRState | undefined;
}) => JSX.Element;
... 6 more ...;
useInfiniteQuery: <TPath_3 extends never>(pathAndInput: [path: ...], opts?: UseTRPCInfiniteQueryOptions<...> | undefined) => UseInfiniteQueryResult<...>;
}
19-1-1 ProviderはtRPCのコンテキストを提供するReactコンポーネント。小コンポーネントでtRPCの機能を利用させる
19-1-2 useQuery, useMutation, useInfiniteQuery は、tRPCのエンドポイントを使うためのカスタムフックである。React コンポーネントの中でデータフェッチ、ミューテーションの操作が行えるようになる。
19-1-3 client はTRPCClientのインスタンスである。
20 Next.jsでのページコンポーネントの用意
src/pages/index.tsxをこのようにする
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '@/styles/Home.module.css'
const Home: NextPage = () => {
return (
<div>
</div>
)
}
export default Home
20-1 NextPageという型
NextPageはNext.jsのページコンポーネントが満たすべき要件を表す。
Reactコンポーネントと同じような動作だが、Next.jsの特有なプロパティ(getInitialProps)についての型定義も提供する
20-2 next/headについて
Next.jsが提供するヘッダー管理のためのコンポーネントである。
タグの中身を動的に設定する。(タイトル、 メタデータなど)20-3 next/imageについて
Next.jsが提供する画像コンポーネント。
最適化や遅延ローディングなどの機能が含まれる。
20-4 CSSモジュールについて
JavaScriptモジュールとして、CSSをとりこむのがCSSモジュールである。
Home.module.cssというファイルのスタイルは他のコンポーネントに漏れない。
21 Reactのカスタムフックでデータをエンドポイントから取得
// pages/index.tsx
const Home: NextPage = () => {
const { data, error, isLoading } = trpc.useQuery(['hello'])
//略
}
21-1 trpc.useQueryというReactの、カスタムフック
data, error, isLoadingの値をオブジェクトとして返す。先ほどやったオブジェクトでストラチャリングが使われて三つの変数ができたことになる。{data, error, isLoading} という三つの値を含むオブジェクト(つまり、trpc.useQueryの返り値)自体を表す変数は作られていない。
21-1-1 Hooksについて
React 16.8から登場した機能で「ステート」・「ライフサイクル」を、クラスコンポーネントを書かず関数コンポーネントで使えるようになったもの
おもなフック
useState ⇒ ステートを関数コンポーネントで使用するときのフック
useEffect ⇒ 関数コンポーネントでライフサイクルのメソッドを使えるようにするフック
useContext ⇒ 関数コンポーネントでReactのコンテクストを使用できるようにする
21-1-2 カスタムフックについて
開発者が自分で定義するフックのこと。カスタムフックはJavaScriptの関数であるが、慣例的に関数の名前をuseで始める。コンポーネントに使うロジックを再利用したいとき、カスタムフックを使って共有が行われたりする。
例えば特定のAPIからデータをフェッチするロジックをもったカスタムフックを作成する。そのフックを複数のコンポーネントから呼び出し、データフェッチのロジックを各コンポーネントの間で再利用できる。useStateやuseEffect等の組み込みフックをカスタムフックに取り入れることもできる。
21-1-3 trpc.useQueryについて
trpcは createReactQueryHooks()で作成されている
useQueryフックは、そのオブジェクトの一部として提供される。一応定義が以下にある。
// @trpc/react/src/createReactQueryHooks.tsx
// のcreateReactQueryの、useQuery。
function useQuery<
TPath extends keyof TQueryValues & string,
TQueryFnData = TQueryValues[TPath]['output'],
TData = TQueryValues[TPath]['output'],
>(
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
opts?: UseTRPCQueryOptions<
TPath,
TQueryValues[TPath]['input'],
TQueryFnData,
TData,
TError
>,
): UseQueryResult<TData, TError> {
const { client, isPrepass, queryClient, prefetchQuery } = useContext();
if (
typeof window === 'undefined' &&
isPrepass &&
opts?.ssr !== false &&
opts?.enabled !== false &&
!queryClient.getQueryCache().find(pathAndInput)
) {
prefetchQuery(pathAndInput as any, opts as any);
}
const actualOpts = useSSRQueryOptionsIfNeeded(pathAndInput, opts);
return __useQuery(
pathAndInput as any,
() => (client as any).query(...getClientArgs(pathAndInput, actualOpts)),
actualOpts,
);
}
22 エラーの処理
const Home: NextPage = () => {
//略
if(isLoading) {
return <p>Loading...</p>
}
if(error) {
return <div>{JSON.stringify(error)}</div>
}
return (
<div>{JSON.stringify(data)}</div>
)
}
22-1 isLoadingはbooleanなのか?
booleanであるっぽい。
status === ‘loading’としても同じ効果が得られる。なので今回の場合こう書いても同じ。
const Home: NextPage = () => {
//const { data, error, isLoading } = trpc.useQuery(['hello'])
const { status, data, error } = trpc.useQuery(['hello'])
if(status === 'loading') {
return <p>Loading...</p>
}
if(status === 'error') {
return <div>{JSON.stringify(error)}</div>
}
return (
<div>{JSON.stringify(data)}</div>
)
}
23 yarn devして開発サーバを起動する → エラーを確認する
{"originalError":{},"isDone":false,"name":"TRPCClientError"}
これはsrc/apiの下に何もファイルを置いていないからである
24 Next.jsのAPIルートとtRPCの連携を行う
// src/api/trpc/[trpc].ts
import * as trpcNext from '@trpc/server/adapters/next'
export default trpcNext
24-1 Next.jsのAPIルート + tRPCのサーバ するためのアダプター
'@trpc/server/adapters/next'を使う。
それをtrpcNextとしてエクスポートする。
これでNext.jsのフロントエンドから、trpc.useQuery, trpc.useMutationなどのフックを使い、tRPCのエンドポイントを呼びだすことができる
これでAPIのリクエスト・レスポンスにTypeScriptの型を導入できる。
25 Next.jsでAPIハンドラを作り、設定を行う
import { appRouter } from '../../../server/route/app.router'
import * as trpcNext from '@trpc/server/adapters/next'
import { createContext } from '../../../server/createContext'
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
onError({error}){
if(error.code === 'INTERNAL_SERVER_ERROR'){
console.log('Something went wrong', error)
}else{
console.log(error)
}
}
})
25-1 Next.jsのAPIハンドラを作成する
trpcNext.createNextApiHandlerメソッドで行う。
25-2 routerの指定
appRouterを指定する。先ほどから作成してきたtRPCのルータである。
25-3 createContext.tsで作ったcreateContextを使う
コンテクストオブジェクトにはreq, resが含まれていてそれらの方はNextApiRequest, NextApiResponseだった(おさらい)
このオブジェクトが、tRPCルータの、各エンドポイントのハンドラに渡される。
25-4 @の使い方
@scope/packageという形をとる。
@scopeとは、パッケージを公開する個人・団体の名前を表し、npmで一般的に使用されるパッケージ名の形式である。
26 yarn dev のちlocalhost:3000でブラウザの更新をして、ネットワークタブで
http://localhost:3000に接続するとこのようになる
(Chromeの開発者ツールを開いている間レスポンスが帰ってこないエラーあり。Safariでは大丈夫)
27 localhost:3000/api/trpc/helloに接続する
これでとりあえずtRPCのhelloのエンドポイントをテストできた。