弊社では、現在プロダクトの技術スタックに tRPC を導入することを検討しています。
その検討で得た知見をもとに、本記事では tRPC を使って簡単な React TODO アプリを作り方をお伝えしたいと思います。
tRPC は日本語記事が少ないので、この記事を参考に tRPC を試してみていただけると嬉しいです。
tRPC とは?
TypeScript を使って型安全な Web API 通信を簡単に実装できる BFF フレームワークです。
直近で GitHub の star 数が急上昇しており、フロントエンド界隈ではワールドワイドで盛り上がっているプロジェクトです (以下 GitHub ページより)。
類似の BFF フレームワークとしては GraphQL が有名ですが、tRPC でも GraphQL とほとんど同様のことができます。
GraphQL と比較した場合の tRPC 特徴はざっくり以下のような感じです。
- TypeScript 限定 (Web や React Native 等での利用が想定されている)
- 軽量 (公式ページによると
🍃 Light - tRPC has zero deps and a tiny client-side footprint.
) - 学習コストが低い
特に学習コストが GraphQL に比べて低いという点は tRPC の大きなアドバンテージです。
GraphQL や gRPC ではクライアント / サーバー間の API 通信を行うために独自のクエリで API を定義する必要がありますが、tRPC ではそのようなことは不要です。そのためサクッと実装することができ、開発体験が非常に良いです。
ではどのようにしてクライアント・サーバー間で型安全な API 通信を実現しているかというと、具体的には tRPC ではサーバー側で実装した router モジュールの型をエクスポートして、クライアント側で利用します。
// サーバー側のモジュール
// (公式ページからの引用)
import * as trpc from '@trpc/server';
const appRouter = trpc.router();
// only export *type signature* of router!
// to avoid accidentally importing your API
// into client-side code
export type AppRouter = typeof appRouter; // この型をクライアント側で使用する
また公式ページのドキュメント量も多くないので、理解しやすいです。
以降で簡単な React TODO アプリを作って、実際の流れを説明したいと思います。
TODO アプリを作る
今回はシンプルに、tRPC サーバー (Express) と API 通信を行って TODO データのやり取りを行う React TODO アプリ (tRPC クライアント) を作ります。
tRPC サーバーの実装
まず tRPC ライブラリをインストールします。
# zod は必須ではないですが、公式ページでも使用されているバリデーション ライブラリです
yarn add @trpc/server zod
tRPC では router を生成し、router の query/mutation メソッドに API を実装します。
コードの内容をシンプルにするため今回は TODO の一覧を取得する query と、TODO を追加する mutation エンドポイントの 2 つだけを実装します。
import trpc from "@trpc/server";
export const appRouter = trpc
.router() // ルーターの生成
.query("getTodoList", { // エンドポイント名: getTodoList
// resolve() 内に実装を書いていく (todoService の実装は割愛)
async resolve(): Promise<Todo[]> {
return await todoService.getAll();
},
})
.mutation("addTodo", { // エンドポイント名: addTodo
// input 内にユーザーインプットのバリデーションを書いていく
// この例では zod を使って { text: string} をバリデーション
input: z.object({
text: z.string(),
}),
async resolve(req): Promise<Todo> {
// 上で定義した input が req に格納されている
// 不正な input であった場合は、この手前で HTTP400 エラーが返される
const { text } = req.input;
const id = uuid();
return await todoService.addTodo(id, text);
},
})
export type AppRouter = typeof appRouter;
以上で router の実装は完了です。
tRPC は Express サーバーまたは Next.js に middleware として取り入れることが可能なので、既存の Express/Next.js プロジェクトに tRPC を追加することも容易に可能です。
Express に tRPC を追加するためには、createExpressMiddleware 関数を使用します。
このとき、リクエスト発生時にコンテキストを生成する createContext 関数も引数として渡す必要があるので、一緒に実装します。
(今回はコンテキストは使わないので、空の関数とします。コンテキストに関してはまた別の投稿で!)
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { appRouter as router } from "./router";
const app = express();
// リクエストごとにコンテキストを生成する関数
const createContext = ({
req,
res,
}: trpcExpress.CreateExpressContextOptions) => ({});
// middlware として tRPC router を追加
// path は任意ですが、公式と合わせて /trpc とします。
app.use(
'/trpc',
createExpressMiddleware({ router, createContext })
);
app.listen(4000);
以上でサーバー側の実装は終わりです。
tRPC クライアント (React) の実装
まずはクライアント用ライブラリをインストールします。
tRPC の React ライブラリは TanStack Query (React Query) を内部で使用するため、一緒にインストールします。
注意点としてはクライアントにも @trpc/server が必要です。これがないと query や mutation を行った際に取得したデータの型が解決できません。
yarn add @trpc/client @trpc/server @trpc/react react-query@3
クライアント側では createReactQueryHooks 関数で hook を作成します。
この際にサーバー側でエクスポートした router の型をジェネリクスに含めることで、vscode などのエディターで補完が聞いて簡単に実装を書くことができます。
import { createReactQueryHooks } from "@trpc/react";
// サーバー側の router.ts からエクスポートされた AppRouter をインポート
import type { AppRouter } from "../../../server/router";
export const trpc = createReactQueryHooks<AppRouter>();
生成した hook を使用してまずは App.tsx 上でプロバイダーの設定を行います。
import { useState } from "react";
import { QueryClientProvider, QueryClient } from "react-query";
import { trpc } from "./trpc";
import { Form } from "./Form";
import { TodoList } from "./TodoList";
import "./App.css";
const url = "http://localhost:3000/trpc";
const client = new QueryClient();
function App() {
const [trpcClient] = useState(() => trpc.createClient({ url }));
return (
<trpc.Provider queryClient={client} client={trpcClient}>
<QueryClientProvider client={client}>
<div>
<TodoList />
<Form />
</div>
</QueryClientProvider>
</trpc.Provider>
);
}
export default App;
これで tRPC クライアントを使用する準備ができました!
hook の useQuery 関数を使用して、TODO の一覧を取得するコンポーネントを作成します。
import { trpc } from "./trpc";
export const TodoList = () => {
const { data, isFetched } = trpc.useQuery(["getTodoList"]);
if (!isFetched) return <>loading</>;
return (
<div>
<span>todo list</span>
<ul>
{data?.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
};
hook の useMutation 関数を使用して TODO を追加するコンポーネントも作成します。
import { useRef } from "react";
import { trpc } from "./trpc";
export function Form() {
const mutation = trpc.useMutation("addTodo");
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
if (inputRef.current) {
mutation.mutate({ text: inputRef.current.value });
}
};
return (
<div>
<input ref={inputRef} type="text" placeholder="add something" />
<button onClick={handleClick}>ADD</button>
</div>
);
}
これで完成です!
しかしながらこれだと TODO を追加後に、事前に取得していた TODO リストが動的に更新されないという問題があります。
対処方法として、mutate に onSuccess コールバック関数を渡すことで、mutate 成功時にその関数を実行することができるので、その中で invalidateQueries を呼び出し、事前に取得していた "getTodoList" クエリのキャッシュを invalidate します。
(subscription を使用するなど、もっと良い方法があると思いますが、今回はこの方法で。)
import { useRef } from "react";
import { trpc } from "./trpc";
export function Form() {
const mutation = trpc.useMutation("addTodo");
const inputRef = useRef<HTMLInputElement>(null);
const utils = trpc.useContext();
const handleClick = () => {
if (inputRef.current) {
mutation.mutate(
{ text: inputRef.current.value },
{ onSuccess: () => utils.invalidateQueries("getTodoList") }
);
}
};
return (
<div>
<input ref={inputRef} type="text" placeholder="add something" />
<button onClick={handleClick}>ADD</button>
</div>
);
}
改めて…これで完成です!
次回は、tRPC のもう少し細かい機能に触れたいと思います。
また弊社ではライブラリの作者である daishi さんと共に、React アプリの micro state management に jotai を導入している真っ只中です。
冒頭の経緯もあり、この度 jotai と tRPC を React アプリで使用するためのライブラリ jotai-trpc が爆誕しました!
ので、どこかのタイミングで jota-trpc についても投稿できればと思います。
最後まで読んでいただきありがとうございました