結局のところ、T3 Stackを採用するメリットとは何なのか?フルスタックフレームワーク、T3 Stack入門(終)(2/3 ページ)

» 2025年02月19日 05時00分 公開
[上原潤二NTTテクノクロス]

チュートリアル(Tainwind & RSC)

 さて、今回もT3 StackでTailwind CSSを活用するサンプルコードのチュートリアルを準備してみました。全体はこちらのGitHubリポジトリにあります。また、これまでの連載ではサンプルコードに「Next.js」のPages Routerを選択したコード例を示していましたが、今回はApp Routerでやっていきます。

ステップ1:Create T3 Appで初期プロジェクトを作成する

 [create-t3-app]を使って新しいプロジェクトのひな型を作成します。プロジェクトを作成するディレクトリに移動し、コマンドラインから以下のコマンドを実行してください。なお、以下では[create-t3-app@7.38.1]で実行していますが、最新版で作成したい場合には[create-t3-app@latest]を指定してください。

  1. > npx create-t3-app@7.38.1 t3-todo-app-router
  2. Need to install the following packages:
  3. create-t3-app@7.32.0
  4. Ok to proceed? (y) y
  5. ___ ___ ___ __ _____ ___ _____ ____ __ ___ ___
  6. / __| _ \ __| / \_ _| __| |_ _|__ / / \ | _ \ _ \
  7. | (__| / _| / /\ \| | | _| | | |_ \ / /\ \| _/ _/
  8. \___|_|_\___|_/~~\_\_| |___| |_| |___/ /_/~~\_\_| |_|
  9. Will you be using TypeScript or JavaScript?
  10. TypeScript
  11. Will you be using Tailwind CSS for styling?
  12. Yes
  13. Would you like to use tRPC?
  14. Yes
  15. What authentication provider would you like to use?
  16. None
  17. What database ORM would you like to use?
  18. Prisma
  19. Would you like to use Next.js App Router?
  20. Yes
  21. What database provider would you like to use?
  22. SQLite (LibSQL)
  23. Should we initialize a Git repository and stage the changes?
  24. Yes
  25. Should we run 'npm install' for you?
  26. Yes
  27. What import alias would you like to use?
  28. ~/
  29. Using: npm
  30. t3-todo-app-router scaffolded successfully!
  31. Adding boilerplate...
  32. Successfully setup boilerplate for prisma
  33. Successfully setup boilerplate for tailwind
  34. Successfully setup boilerplate for trpc
  35. Successfully setup boilerplate for envVariables
  36. Successfully setup boilerplate for eslint
  37. Installing dependencies...
  38. Successfully installed dependencies!
  39. Initializing Git...
  40. Successfully initialized and staged git
  41. Next steps:
  42. cd t3-todo-app-router
  43. npm run db:push
  44. npm run dev
  45. git commit -m "initial commit"

 上記でデフォルト以外を選択しているのはORMの選択をNoneからPrismaに指定していることのみです。なお、上記ではDBに「SQLite」を選択していますが、「MySQL」もしくは「Postgres」を選択した場合、スクリプト「start-database.sh」が作成され、Dockerコンテナ内でDBを起動できるようになります。

ステップ2:必要なパッケージのインストール

 必要なパッケージをインストールします。tailwind-variantsパッケージをインストールします。

  1. > cd t3-todo-app-router
  2. > npm install tailwind-variants

ステップ3:DBの初期化

 SQLiteデータベースを初期化します。

  1. > npm run db:push
  2. > t3-todo-app-router@0.1.0 db:push
  3. > prisma db push
  4. Environment variables loaded from .env
  5. Prisma schema loaded from prisma/schema.prisma
  6. Datasource "db": SQLite database "db.sqlite" at "file:./db.sqlite"
  7. SQLite database db.sqlite created at file:./db.sqlite
  8. Your database is now in sync with your Prisma schema. Done in 23ms
  9. Generated Prisma Client (v5.22.0) to ./node_modules/@prisma/client in 102ms

ステップ4: アプリケーションの実行確認

 プロジェクトディレクトリで、以下のコマンドを実行してアプリケーションを起動します。

  1. > npm run dev
  2. > t3-todo-app-router@0.1.0 dev
  3. > next dev --turbo
  4. Next.js 15.0.3 (Turbopack)
  5. - Local: http://localhost:3000
  6. - Environments: .env
  7. Starting...
  8. Ready in 5.2s

 初期状態でWebアプリが実行されます。Webブラウザでhttp://localhost:3000にアクセスし、デフォルトの表示である以下が確認できればOKです。

ステップ5:初期生成ファイルのうち不要なファイルを削除する

 このチュートリアルでは不要になる以下のファイルを削除しておきます。

  1. > rm src/server/api/routers/post.ts
  2. > rm src/app/_components/post.tsx

ステップ6:Prismaのデータベーススキーマを修正する

 以下のようにprisma/schema.prismaに修正を加えます。

  1. -model Post {
  2. - id Int @id @default(autoincrement())
  3. - name String
  4. - createdAt DateTime @default(now())
  5. - updatedAt DateTime @updatedAt
  6. -
  7. - @@index([name])
  8. - }
  9. +model Todo {
  10. + id String @id @default(cuid())
  11. + done Boolean
  12. + text String
  13. + createdAt DateTime @default(now())
  14. + updatedAt DateTime @updatedAt
  15. + }

 スキーマを変更したのでPrisma Clientを再生成し、またDB内のスキーマも更新します。

  1. > npm run db:push

ステップ7:tRPCルーター定義(サーバサイド)

 以下の内容でsrc/server/api/routers/todo.tsを作成します。

  1. import { z } from "zod";
  2. import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
  3. export const todoRouter = createTRPCRouter({
  4. getAll: publicProcedure.input(z.void()).query(async ({ ctx }) => {
  5. return ctx.db.todo.findMany({ orderBy: [{ createdAt: "desc" }] });
  6. }),
  7. add: publicProcedure
  8. .input(
  9. z.object({
  10. text: z.string(),
  11. }),
  12. )
  13. .mutation(async ({ ctx, input }) => {
  14. const { text } = input;
  15. return ctx.db.todo.create({
  16. data: {
  17. done: false,
  18. text,
  19. },
  20. });
  21. }),
  22. delete: publicProcedure
  23. .input(z.object({ id: z.string() }))
  24. .mutation(({ ctx, input }) => {
  25. const { id } = input;
  26. return ctx.db.todo.delete({ where: { id } });
  27. }),
  28. done: publicProcedure
  29. .input(z.object({ id: z.string(), done: z.boolean() }))
  30. .mutation(({ ctx, input }) => {
  31. const { id, done } = input;
  32. console.log(`id=`, id, ` done=`, done);
  33. return ctx.db.todo.update({ where: { id }, data: { done } });
  34. }),
  35. });

 これに合わせて以下のようにsrc/server/api/root.tsを修正します。

  1. - import { postRouter } from "~/server/api/routers/post";
  2. + import { todoRouter } from "~/server/api/routers/todo";
  3. import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
  4. /**
  5. * This is the primary router for your server.
  6. */
  7. export const appRouter = createTRPCRouter({
  8. - post: postRouter,
  9. + todo: todoRouter,
  10. });
  11. // export type definition of API
  12. :

ステップ8: Tailwind.CSSの設定

 以下の修正をtailwind.config.tsに加えます。これで画像のような影付きの効果を与えたshadow-neonというユーティリティークラスが指定できるようになります。

  1. fontFamily: {
  2. sans: ["var(--font-sans)", ...fontFamily.sans],
  3. },
  4. + boxShadow: {
  5. + neon: "0 0 2px #fff, inset 0 0 3px #fff, 0 0 5px #787, 0 0 15px #787, 0 0 30px #787",
  6. + },
  7. },
  8. },
  9. plugins: [],

ステップ9:UIを作成する(1)

 以下の内容でsrc/app/page.tsxを置き換えます。App Routerでのトップページであるサーバコンポーネントです。

  1. import { type NextPage } from "next";
  2. import TodoList from "~/app/_components/TodoList";
  3. const TodoApp: NextPage = async () => {
  4. return (
  5. <main className="min-h-screen bg-slate-950 p-2">
  6. <div className="container mx-auto flex flex-col p-4">
  7. <h1 className="border-2 border-amber-500 p-3 text-center text-4xl font-bold text-amber-500 shadow-neon">
  8. Todoアプリ
  9. </h1>
  10. <br />
  11. <TodoList />
  12. </div>
  13. </main>
  14. );
  15. };
  16. export default TodoApp;

 以降、使用する以下のReactコンポーネント群をsrc/_components配下に作成していきます。全てクライアントコンポーネントとして定義します。

  • src/app/_components/TodoList.tsx
  • src/app/_components/TodoItem.tsx
  • src/app/_components/CreateTodo.tsx
  • src/app/_components/Button.tsx

 これらのコンポーネントを使用した画面構成は以下の通りです。

ステップ10:UIを作成する(2)

 src/app/_components/TodoList.tsxを以下の内容で作成します。画面下部のタスク一覧を表示するクライアントコンポーネントです。今回はSupense配下で実行するためにuseSuspenseQueryを呼び出します。

  1. "use client";
  2. import { api } from "~/trpc/react";
  3. import TodoItem from "./TodoItem";
  4. import CreateTodo from "./CreateTodo";
  5. export default function TodoList() {
  6. const [todos] = api.todo.getAll.useSuspenseQuery();
  7. return (
  8. <>
  9. <CreateTodo />
  10. <ul id="taskList" className="list-inside list-disc shadow-neon">
  11. {todos?.map((todo) => (
  12. <li
  13. key={todo.id}
  14. className="mb-2 flex items-center rounded p-2 text-white"
  15. >
  16. <TodoItem todo={todo} />
  17. </li>
  18. ))}
  19. </ul>
  20. </>
  21. );
  22. }

ステップ11:UIを作成する(3)

 src/app/_components/TodoItem.tsxを作成します。これはタスク一覧の要素であるタスクを表示するクライアントコンポーネントです。

  1. "use client";
  2. import { type RouterOutputs, api } from "~/trpc/react";
  3. import { Button } from "./Button";
  4. type Props = {
  5. todo: RouterOutputs["todo"]["getAll"][0]; //(1)
  6. };
  7. export default function TodoItem({ todo }: Props) {
  8. const utils = api.useUtils();
  9. const { mutateAsync: todoDeleteAsync } = api.todo.delete.useMutation({
  10. onSettled: () => {
  11. void utils.todo.invalidate();
  12. },
  13. });
  14. const { mutateAsync: todoDoneAsync } = api.todo.done.useMutation({
  15. onSettled: () => {
  16. void utils.todo.invalidate();
  17. },
  18. });
  19. function handleDeleteTodo(id: string) {
  20. void todoDeleteAsync({ id });
  21. }
  22. function handleDoneTodo(id: string, done: boolean) {
  23. void todoDoneAsync({ id, done });
  24. }
  25. return (
  26. <>
  27. <input
  28. type="checkbox"
  29. className="ml-2 mr-4 accent-pink-500 shadow-neon"
  30. checked={todo.done}
  31. onChange={() => handleDoneTodo(todo.id, !todo.done)}
  32. />
  33. <span className={todo.done ? "line-through" : ""}>{todo.text}</span>
  34. <Button
  35. variant={"danger"}
  36. size={"sm"}
  37. className={"ml-auto"}
  38. onClick={() => handleDeleteTodo(todo.id)}
  39. >
  40. ×
  41. </Button>
  42. </>
  43. );
  44. }

 コンポーネントのpropsであるtodoの型はtRPCのtRPCのルーター定義から取得します。(1)のRouterOutputsはCreate T3 Appが定義しているユーティリティー型であり、tRPCのプロシージャの返り値の型を得ることができます。

ステップ12:UIを作成する(4)

 src/app/_components/CreateTodo.tsxを以下の内容で作成します。画面上部のタスク入力のためのクライアントコンポーネントです。

  1. "use client";
  2. import type { FormEvent } from "react";
  3. import { api } from "~/trpc/react";
  4. import { Button } from "./Button";
  5. export default function CreateTodo() {
  6. const utils = api.useUtils();
  7. const { mutateAsync: todoAddAsync } = api.todo.add.useMutation({
  8. onSettled: () => {
  9. void utils.todo.invalidate();
  10. },
  11. });
  12. function handleAddTodo(e: FormEvent) {
  13. e.preventDefault();
  14. const form = e.target as HTMLFormElement;
  15. const formData = new FormData(form);
  16. const formJson = Object.fromEntries(formData.entries());
  17. form.reset();
  18. void todoAddAsync(formJson as { text: string });
  19. }
  20. return (
  21. <form className="flex" onSubmit={handleAddTodo}>
  22. <input
  23. className="shadow-neon mb-4 mr-4 flex-grow rounded border-2 bg-slate-950 p-2 text-white"
  24. type="text"
  25. name="text"
  26. placeholder="新しいタスクを入力"
  27. />
  28. <Button type="submit">タスクを追加</Button>
  29. </form>
  30. );
  31. }

ステップ13:UIを作成する(5)

 src/app/_components/Button.tsxを以下の内容で作成します。各部のボタンのための共通コンポーネントです。ボタン種別を表すvariantとして、primaryかdangerを選択できます。また、大きさはsm、md、lgの3種類が指定できます。

 この設定によってTailwind CSSのユーティリティークラスの指定を切り替えますが、切り替え処理にTailwind Variantsを使用しています。このコンポーネントはclassName propsを取り、補足的に任意のクラス指定を追加することもできます。

  1. import React, { type ComponentProps } from "react";
  2. import { tv, type VariantProps } from "tailwind-variants";
  3. export const button = tv({
  4. base: `shadow-neon rounded bg-slate-950 font-bold border-2 mb-4 `,
  5. variants: {
  6. variant: {
  7. primary: `border-blue-400 text-blue-500 hover:bg-blue-700`,
  8. danger: `border-red-400 text-red-300 hover:bg-red-700"`,
  9. },
  10. size: {
  11. sm: `px-2 py-1`,
  12. md: `px-4 py-2`,
  13. lg: `px-6 py-4`,
  14. },
  15. },
  16. defaultVariants: {
  17. variant: "primary",
  18. size: "md",
  19. },
  20. });
  21. type ButtonProps = ComponentProps<"button"> &
  22. VariantProps<typeof button> & {
  23. children: React.ReactNode;
  24. };
  25. export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  26. (
  27. { className, variant, size, children, disabled, type = "button", ...props },
  28. ref,
  29. ) => {
  30. return (
  31. <button
  32. type={type}
  33. className={button({ variant, className, size })}
  34. ref={ref}
  35. disabled={disabled}
  36. {...props}
  37. >
  38. {children}
  39. </button>
  40. );
  41. },
  42. );
  43. Button.displayName = "Button";

ステップ14:動作確認

 ここでいったん[Ctr]+[C]キーを押下してアプリを終了し、再起動します。

  1. ^C
  2. > npm run dev

 以下の画面が表示され、Todoタスクの追加や削除操作ができることを確認します。

サーバサイドtRPCの活用

 ここまでは、トップレベル以外は全てクライアントコンポーネントとして定義してきており、今回選択したApp Routerの主要機能の一つであるReact Server Component(RSC)は使用していませんでした。ここではRSCからtRPCを呼び出すように拡張してみます。

 方針は以下の通りです。

  • Todoタスク一覧をあらかじめサーバサイドで生成して、初期表示を高速化する
    • tRPCのgetAll()をサーバ内で呼び出してタスク一覧を生成するサーバコンポーネントTodoListSC.tsxを定義する
    • SuspenseのフォールバックとしてTodoListSCの結果を表示するように指定する
  • クライアントコンポーネントでtRPCのgetAll()をリモートで呼び出す
    • 呼び出しの結果が結果が返るまでは上記フォールバックを表示し、取得完了後に最新取得した一覧に置き換える(Suspenseの機能)

ステップ15:コード修正

 先の方針に従い、src/app/page.tsxに以下の修正を反映します。

  1. import { type NextPage } from "next";
  2. + import TodoListSC from "~/app/_components/TodoListSC";
  3. import TodoList from "~/app/_components/TodoList";
  4. + import { Suspense } from "react";
  5. const TodoApp: NextPage = async () => {
  6. return (
  7. <main className="min-h-screen bg-slate-950 p-2">
  8. <div className="container mx-auto flex flex-col p-4">
  9. <h1 className="border-2 border-amber-500 p-3 text-center text-4xl font-bold text-amber-500 shadow-neon">
  10. Todoアプリ
  11. </h1>
  12. <br />
  13. - <TodoList />
  14. + <Suspense fallback={<TodoListSC />}>
  15. + <TodoList />
  16. + </Suspense>
  17. </div>
  18. </main>
  19. );
  20. };
  21. export default TodoApp;

ステップ16:TodoListSC.tsxの作成

 src/app/_components/TodoListSC.tsxを以下の内容で作成します。

  1. import { api } from "~/trpc/server";
  2. import TodoItem from "~/app/_components/TodoItem";
  3. import CreateTodo from "./CreateTodo";
  4. export default async function TodoListSC() {
  5. const todos = await api.todo.getAll();
  6. return (
  7. <>
  8. <CreateTodo />
  9. <ul
  10. id="taskList"
  11. className="list-inside list-disc bg-blue-800 shadow-neon"
  12. >
  13. {todos.map((todo) => (
  14. <li
  15. key={todo.id}
  16. className="mb-2 flex items-center rounded p-2 text-white"
  17. >
  18. <TodoItem todo={todo} />
  19. </li>
  20. ))}
  21. </ul>
  22. </>
  23. );
  24. }

Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

Coding Edge 鬯ッ�ョ�ス�ォ�ス�ス�ス�ェ鬮ッ蛹コ�サ繧托スス�ソ�ス�ス�ス�ス�ス�ス�ス�ス�ス�コ鬮」蛹�スス�オ髫エ竏オ�コ�キ�ス�ク�ス�キ�ス�ス�ス�ケ髫エ雜」�ス�「�ス�ス�ス�ス�ス�ス�ス�ウ鬯ゥ蟷「�ス�「�ス�ス�ス�ァ�ス�ス�ス�ス�ス�ス�ス�ュ鬯ゥ蟷「�ス�「髫エ雜」�ス�「�ス�ス�ス�ス�ス�ス�ス�ウ鬯ゥ蟷「�ス�「�ス�ス�ス�ァ�ス�ス�ス�ス�ス�ス�ス�ー

鬯ョ�ォ�ス�エ髯晢スキ�ス�「�ス�ス�ス�ス�ス�ス�ス�ャ鬯ョ�ォ�ス�エ鬯イ�ス�シ螟イ�ス�ス�ス�ス�ス�ス�ス�・鬯ョ�ォ�ス�エ髯晢スカ�ス�キ�ス�ス�ス�」�ス�ス�ス�ッ鬮」蜴�スス�ォ�ス�ス�ス�」

注目のテーマ

4AI by @IT - AIを作り、動かし、守り、生かす
Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。