こんにちは。 アドベントカレンダー23日目です。終盤に差し掛かってきました。 今年はフロントエンドまわりであれこれ開発することが多かったので、振り返りを兼ねてこれからフロントエンド開発を始める方向けに入門編としてお送りしています。 最終日の25日には何かアプリケーションができていることが目標です。
↓最初から読み始めたい方はこちらか↓
おさらい
- Visual Studio Codeを使ったReactの開発環境の構築(第1回)
- CSSフレームワークの1つであるtailwindcssを使ったUI開発(第2回)
- コンポーネント化(第3回)
- フォーマッター(第4回)
- Linter(第5回)
- バージョン管理(第6回)
- 状態管理とReact hook form(第7回、第8回、第11回))
- Visual Studio Codeの設定(第9回、第10回、第16回、第17回)
- Reactルーティング(第12回)
- Playwright(第13回、第14回)
- GitHub CodespacesでAmplify
- 環境立ち上げとCLIツールの有効化(第15回)
- Amplifyで初期プロジェクト作成(第18回)
- Amplifyで認証機能作成(第19回)
- AmplifyでAPI機能作成(第20回)
- Amplifyで書籍登録機能(第21回)
- Amplifyで書籍取得機能(第22回)
今回のはなし
第18回からAmplifyシリーズでお送りしています。「書籍管理アプリ」を作りながらAPI機能の基本的な使い方を学んでいます。前回は、書籍取得の処理を作ってみました。今回は登録した書籍情報を編集できるように更新機能を追加していきましょう。
画面UIの作成
まずは画面UIから作成します。既存ファイルの編集といくつかファイルを作成しますが、順番に説明します。
pages ├── app.css ├── _app.tsx ├── book │ ├── createBookData.ts + │ ├── [id] + │ │ ├── edit.tsx + │ │ ├── fetchbook.ts + │ │ └── updateBookData.ts │ └── resister.tsx ├── fetchBooks.ts └── index.tsx
編集画面
最初に編集画面を作成します。先に編集対象を取得するための関数と変更した内容を更新する関数の2種類を作成します。
1つ目の取得する関数です。前回の記事でリスト取得は扱いましたが、今回はget
の方です。get
では、idをvariables
に渡してあげることで取得することができます。それ以外のコードは毎度同じようなことをやっているため、省略します。
import { Book, GetBookQuery } from "@/src/API"; import { getBook } from "@/src/graphql/queries"; import { GraphQLResult, generateClient } from "aws-amplify/api"; export default async function fetchBook(id: string) { const client = generateClient(); const response = await client.graphql({ query: getBook, variables: { id, }, }) as GraphQLResult<GetBookQuery>; if (response.errors) { throw new Error(response.errors[0].message); } if (!response.data?.getBook) { throw new Error("Book not found"); } const book: Book = response.data?.getBook; return book; }
次に更新する処理です。更新する処理はupdateBook
をクエリに渡します。細かい説明は省略します。
import { UpdateBookInput, UpdateBookMutation } from "@/src/API"; import { updateBook } from "@/src/graphql/mutations"; import { GraphQLResult, generateClient } from "aws-amplify/api"; export default async function updateBookData(input: UpdateBookInput) { const client = generateClient(); const response = await client.graphql({ query: updateBook, variables: { input }, }) as GraphQLResult<UpdateBookMutation>; if (response.errors) { throw new Error(response.errors[0].message); } if (!response.data?.updateBook) { throw new Error("Book not updated"); } const book = response.data?.updateBook; return book; }
ここまで準備できたら編集画面です。次のコードをコピー貼り付けてください。
サンプルコードはこちら
"use client";
import { CreateBookInput } from "@/src/API";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import fetchBook from "./fetchbook";
import updateBookData from "./updateBookData";
type AutherInputs = {
name: CreateBookInput["name"];
}
type Inputs = {
id: string;
name: CreateBookInput["name"];
authers: AutherInputs[];
publisher: CreateBookInput["publisher"];
};
export default function Edit() {
const router = useRouter();
const { id } = router.query;
const {
register,
control,
handleSubmit,
setValue,
formState: {
isValid,
isDirty,
isSubmitting,
}
} = useForm<Inputs>({
mode: "onChange",
defaultValues: {
name: "",
authers: [],
publisher: "",
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "authers",
});
useEffect(() => {
if (!id || typeof id === "object") {
return;
}
fetchBook(id as string).then((book) => {
setValue("id", book.id);
setValue("name", book.name);
setValue("authers", book.auther
?.filter((item): item is NonNullable<typeof item> => !!item)
.map((name) => ({ name })) || []);
setValue("publisher", book.publisher);
}).catch((error) => {
console.error(error);
});
}, [id]);
const onSubmit = (data: Inputs) => {
updateBookData({
id: data.id,
name: data.name,
auther: data.authers
.map((auther) => auther.name)
.filter((item): item is NonNullable<typeof item> => !!item),
publisher: data.publisher,
})
.then(() => {
alert("登録しました");
router.push("/");
}).catch((error) => {
alert("登録に失敗しました");
});
}
return (
<div className="space-y-5">
<h3 className="text-3xl font-bold dark:text-white">書籍編集</h3>
<div className="space-y-3">
<div>
<label
htmlFor="book_name"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
書籍名
</label>
<input
id="book_name"
type="text"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="サンプルの本"
{...register("name", { required: true })}
/>
</div>
<div className="space-y-2">
<label
htmlFor="author"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
著者
</label>
{fields.map((field, index) => (
<div
key={index}
className="flex space-x-2"
>
<input
id="author"
type="text"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="著者"
{...register(`authers.${index}.name`, { required: true })}
/>
<button
type="button"
className="text-red-500 hover:text-red-700 whitespace-nowrap"
onClick={() => remove(index)}
>
削除
</button>
</div>
))}
<button
type="button"
className="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded"
onClick={() => append({ name: "" })}
>
著者を追加
</button>
</div>
<div>
<label
htmlFor="publisher"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
出版社
</label>
<input
id="publisher"
type="text"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="出版社"
{...register("publisher", { required: true })}
/>
</div>
<div>
<button
type="submit"
disabled={!isValid || !isDirty || isSubmitting}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-lg disabled:opacity-50"
onClick={handleSubmit(onSubmit)}
>
更新
</button>
</div>
</div>
</div>
)
}
基本的には登録画面と同じような内容ですので、コピーして使いまわせるところが多いです。違う点だけ説明します。
type Inputs = {
+ id: string;
name: CreateBookInput["name"];
authers: AutherInputs[];
publisher: CreateBookInput["publisher"];
};
登録以外はIDを使って更新処理を実行するため、idを保持できるように項目を追加しています。
export default function Edit() { + const router = useRouter(); + const { id } = router.query; ... }
一覧画面から遷移する際に書籍のIDをURLのパラメーターとして渡します。それを受け取るための処理がuseRouter()
です。
...
const {
register,
control,
handleSubmit,
+ setValue,
formState: {
isValid,
isDirty,
isSubmitting,
}
} = useForm<Inputs>({
mode: "onChange",
defaultValues: {
name: "",
authers: [],
publisher: "",
},
});
...
編集対象のデータを取得後にフォームに内容を反させるために、渡す用の関数です。後で使用します。
useEffect(() => { if (!id || typeof id === "object") { return; } fetchBook(id as string).then((book) => { setValue("id", book.id); setValue("name", book.name); setValue("authers", book.auther ?.filter((item): item is NonNullable<typeof item> => !!item) .map((name) => ({ name })) || []); setValue("publisher", book.publisher); }).catch((error) => { console.error(error); }); }, [id]);
前回軽く触れたuseEffect()
を使っています。第2引数の配列に変数名を渡すことで、その変数が変更されたときに第1引数に渡している関数が実行されます。関数の中では、クエリパラメーターターから取得したIDを用いて書籍情報を取得する処理を実行しています。
const onSubmit = (data: Inputs) => { updateBookData({ id: data.id, name: data.name, auther: data.authers .map((auther) => auther.name) .filter((item): item is NonNullable<typeof item> => !!item), publisher: data.publisher, }) .then(() => { alert("登録しました"); router.push("/"); }).catch((error) => { alert("登録に失敗しました"); }); }
登録処理では、更新処理を呼び出しています。今回は、アラートを表示したり、成功時に一覧画面に遷移する処理を追加しました。
これで編集画面の準備はおわりです。
一覧画面
一覧画面から編集画面に遷移するためのリンクを追加します。pages/index.tsx
を開き次のように変更します。
"use client"; import useBook from "@/hooks/useBook"; import Link from "next/link"; export default function Home() { ... return ( <div className="space-y-5"> ... <div> <table className="border-collapse border border-slate-400 table-auto"> <thead> <tr> <td className="border border-slate-300 p-2 text-center font-bold bg-slate-50">ID</td> <td className="border border-slate-300 p-2 text-center font-bold bg-slate-50">ISBN</td> <td className="border border-slate-300 p-2 text-center font-bold bg-slate-50">書籍名</td> <td className="border border-slate-300 p-2 text-center font-bold bg-slate-50">著者</td> <td className="border border-slate-300 p-2 text-center font-bold bg-slate-50">出版社</td> + <td className="border border-slate-300 p-2 text-center font-bold bg-slate-50">操作</td> </tr> </thead> <tbody> {books.map((book) => ( <tr> <td className="border border-slate-300 p-2">{book.id}</td> <td className="border border-slate-300 p-2">{book.isbn || "(なし)"}</td> <td className="border border-slate-300 p-2">{book.name}</td> <td className="border border-slate-300 p-2"> {book.auther?.filter((item): item is NonNullable<typeof item> => !!item).join(", ")} </td> <td className="border border-slate-300 p-2">{book.publisher}</td> + <td className="border border-slate-300 p-2"> + <Link + href={`/book/${encodeURIComponent(book.id)}/edit`} + className="text-blue-500 hover:text-blue-700" + > + 編集 + </Link> + </td> </tr> ))} </tbody> </table> </div> </div> ); }
これで一覧画面から編集画面へ遷移できるようになりました。
おわりに
今回までで、登録、取得、変更までできるようになりました。CRUDという意味では最後に削除が残っていますが、次回に続く。ということで今回は以上です。