Next.js でUIを構築する
前のチャプターで作ったAPIで、UIの実装をしていきます。
Railsチュートリアル にあるような、ユーザーの一覧ページを作ります。
Pages
src/pages/users.tsx
ファイルを作成し Pages を実装します
import type { NextPage } from "next";
import React, { useState } from "react";
import { trpc } from "../utils/trpc";
const UserIndexPage: NextPage = () => {
const getUsers = trpc.user.getUsers.useQuery();
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
if (!getUsers.data) {
return <div>Loading...</div>;
}
return (
<>
<main className="container p-4 mx-auto max-w-screen-lg">
<div className="flex flex-row justify-center">
<div className="w-1/2">
{getUsers.data.map((user) => (
<UserRow
key={user.id}
name={user.name ?? "--"}
onSelectUser={() => setSelectedUserId(user.id)}
isSelected={selectedUserId === user.id}
/>
))}
</div>
<div className="w-1/2 ml-2 px-2 border-l-gray-200 border-l-2">
<div className="font-bold">詳細</div>
{!selectedUserId && <div>ユーザーが選択されていません</div>}
</div>
</div>
</main>
</>
);
};
const UserRow: React.FC<{ name: string; onSelectUser: () => void, isSelected: boolean }> = ({
name,
onSelectUser,
isSelected,
}) => {
return (
<div className="flex flex-row py-2 space-x-3">
<div>
{isSelected ? "✓" : ""}
{name}
</div>
<div>
<button type="button" className="text-blue-700" onClick={onSelectUser}>
選択
</button>
</div>
</div>
);
};
export default UserIndexPage;
左側にユーザー一覧を出し、右側にそのユーザーの詳細を出すことにしました。ユーザーの詳細ではそのユーザーのMicropostを表示します。
useQuery
const getUsers = trpc.user.getUsers.useQuery();
この行で、前チャプターで定義したprocedureからユーザーを取得しています。useQueryの実装は React Query(TanStack Query) の実装です。useQuery() しているコンポーネントがマウントされたときにgetUsers procedureを呼び出しユーザーを取得しにいきます。
ユーザーのリストが取れたらそれをレンダリングしてあげるだけです。
tailwind
<main className="container p-4 mx-auto max-w-screen-lg">
こちらで当てているcssクラスは tailwindcss のものです。tailwindはユーティリティファーストのcssライブラリで、ここでは説明を割愛します。
ユーザーの詳細を実装する
diff --git a/src/pages/users.tsx b/src/pages/users.tsx
index f9f0cc3..01d560c 100644
--- a/src/pages/users.tsx
+++ b/src/pages/users.tsx
@@ -6,6 +6,11 @@ const UserIndexPage: NextPage = () => {
const getUsers = trpc.user.getUsers.useQuery();
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
+ const getUserMicroposts = trpc.micropost.getUserMicroposts.useQuery(
+ { userId: selectedUserId ?? "never" },
+ { enabled: selectedUserId !== null }
+ );
+
if (!getUsers.data) {
return <div>Loading...</div>;
}
@@ -28,6 +33,9 @@ const UserIndexPage: NextPage = () => {
<div className="font-bold">詳細</div>
{!selectedUserId && <div>ユーザーが選択されていません</div>}
+ {getUserMicroposts?.data?.map((micropost) => (
+ <div key={micropost.id}>- {micropost.content}</div>
+ ))}
</div>
</div>
</main>
ユーザーが選択されている場合にのみ getUserMicropostsが実行されて、ユーザーに紐付いたmicropostsが取得されます。
Micropostを登録する
diff --git a/src/pages/users.tsx b/src/pages/users.tsx
index 1ebd7b3..f335f07 100644
--- a/src/pages/users.tsx
+++ b/src/pages/users.tsx
@@ -6,6 +6,22 @@ const UserIndexPage: NextPage = () => {
const getUsers = trpc.user.getUsers.useQuery();
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
+ const [inputContent, setInputContent] = useState("");
+
+ const utils = trpc.useContext();
+ const createMicropost = trpc.micropost.createMicropost.useMutation({
+ onSuccess: async () => {
+ setInputContent("");
+ utils.micropost.getUserMicroposts.invalidate();
+ },
+ });
+
+ const onClickContentSubmit = async () => {
+ if (selectedUserId) {
+ createMicropost.mutate({ userId: selectedUserId, content: inputContent });
+ }
+ };
+
const getUserMicroposts = trpc.micropost.getUserMicroposts.useQuery(
{ userId: selectedUserId ?? "never" },
{ enabled: selectedUserId !== null }
@@ -37,6 +53,26 @@ const UserIndexPage: NextPage = () => {
{getUserMicroposts?.data?.map((micropost) => (
<div key={micropost.id}>- {micropost.content}</div>
))}
+
+ {selectedUserId && (
+ <div className="m-2 p-2 border-gray-200 border-2 rounded-sm">
+ <label htmlFor={"content"}>内容</label>
+ <input
+ type="text"
+ id={"content"}
+ value={inputContent}
+ onChange={(e) => setInputContent(e.target.value)}
+ className={"border-gray-100 border-2"}
+ />
+ <button
+ type="button"
+ onClick={onClickContentSubmit}
+ className={"p-1 text-blue-700"}
+ >
+ 登録
+ </button>
+ </div>
+ )}
</div>
</div>
</main>
useMutation() フックを使います。これも実装は React Query(TanStack Query) です。
onSuccess: async () => {
setInputContent("");
utils.micropost.getUserMicroposts.invalidate();
}
useMutationが成功したときにはテキストフィールドの内容を初期化し、 getUserMicropostsを再取得することで見た目に反映させています。
mutationからエラー情報を取り出すこともできます。
diff --git a/src/pages/users.tsx b/src/pages/users.tsx
index f335f07..5fc5873 100644
--- a/src/pages/users.tsx
+++ b/src/pages/users.tsx
@@ -71,6 +71,11 @@ const UserIndexPage: NextPage = () => {
>
登録
</button>
+ {createMicropost.error && (
+ <p className="text-red-800">
+ エラー! {createMicropost.error.message}
+ </p>
+ )}
</div>
)}
</div>
内容を未入力のままサーバにPOSTしてしまった例です。そのままのエラーの構造が出ていますが、こちらは別途チャプターでエラーハンドリングの詳細を解説します。
以上で、データの取得と更新を実装することができました。
特にコード生成の必要もなく、その場ですぐ補完が効いていく体験がとても楽です。
続きます。