tRPC で TODO アプリを作る

弊社では、現在プロダクトの技術スタックに tRPC を導入することを検討しています。
その検討で得た知見をもとに、本記事では tRPC を使って簡単な React TODO アプリを作り方をお伝えしたいと思います。
tRPC は日本語記事が少ないので、この記事を参考に tRPC を試してみていただけると嬉しいです。

[今回のサンプルコードはこちら](https://github.com/hirotake111/tRPC-todo-app)

tRPC とは？
---

[公式ページ](https://trpc.io/)

TypeScript を使って型安全な Web API 通信を簡単に実装できる BFF フレームワークです。
直近で GitHub の star 数が急上昇しており、フロントエンド界隈ではワールドワイドで盛り上がっているプロジェクトです (以下 GitHub ページより)。
![star-history-202297.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2664705/fba51b5d-097d-9200-5439-717f0130de01.png)
類似の 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 モジュールの型をエクスポートして、クライアント側で利用します。

```typescript
// サーバー側のモジュール
// (公式ページからの引用)
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 ライブラリをインストールします。

```bash
# zod は必須ではないですが、公式ページでも使用されているバリデーション ライブラリです
yarn add @trpc/server zod
```

tRPC では router を生成し、router の query/mutation メソッドに API を実装します。
コードの内容をシンプルにするため今回は TODO の一覧を取得する query と、TODO を追加する mutation エンドポイントの 2 つだけを実装します。

```typescript:/server/router.ts
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 関数も引数として渡す必要があるので、一緒に実装します。
(今回はコンテキストは使わないので、空の関数とします。コンテキストに関してはまた別の投稿で！)

```typescript:server/index.ts
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 を行った際に取得したデータの型が解決できません。

```bash
yarn add @trpc/client @trpc/server @trpc/react react-query@3
```

クライアント側では createReactQueryHooks 関数で hook を作成します。
この際にサーバー側でエクスポートした router の型をジェネリクスに含めることで、vscode などのエディターで補完が聞いて簡単に実装を書くことができます。

```typescript:client/trpc.ts
import { createReactQueryHooks } from "@trpc/react";
// サーバー側の router.ts からエクスポートされた AppRouter をインポート
import type { AppRouter } from "../../../server/router";

export const trpc = createReactQueryHooks<AppRouter>();
```

生成した hook を使用してまずは App.tsx 上でプロバイダーの設定を行います。

```react:client/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 の一覧を取得するコンポーネントを作成します。

```react:client/TodoList.tsx
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 を追加するコンポーネントも作成します。

```react:Form.tsx
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 を使用するなど、もっと良い方法があると思いますが、今回はこの方法で。)

```react:Form.tsx
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](https://qiita.com/daishi) さんと共に、React アプリの micro state management に [jotai](https://jotai.org/) を導入している真っ只中です。
冒頭の経緯もあり、この度 jotai と tRPC を React アプリで使用するためのライブラリ [jotai-trpc](https://twitter.com/dai_shi/status/1562798221369708544) が爆誕しました！
ので、どこかのタイミングで jota-trpc についても投稿できればと思います。

最後まで読んでいただきありがとうございました :bow: