とことんDevOps | 日本仮想化技術のDevOps技術情報メディア

DevOpsに関連する技術情報を幅広く提供していきます。

日本仮想化技術がお届けする「とことんDevOps」では、DevOpsに関する技術情報や、日々のDevOps業務の中での検証結果、TipsなどDevOpsのお役立ち情報をお届けします。
主なテーマ: DevOps、CI/CD、コンテナ開発、IaCなど
読者登録と各種SNSのフォローもよろしくお願いいたします。

Amplifyを使ってWebアプリを作ろう(書籍一覧編)

こんにちは。 アドベントカレンダー22日目です。終盤に差し掛かってきました。 今年はフロントエンドまわりであれこれ開発することが多かったので、振り返りを兼ねてこれからフロントエンド開発を始める方向けに入門編としてお送りしています。 最終日の25日には何かアプリケーションができていることが目標です。

↓最初から読み始めたい方はこちらか↓

devops-blog.virtualtech.jp

おさらい

今回のはなし

第18回からAmplifyシリーズでお送りしています。「書籍管理アプリ」を作りながらAPI機能の基本的な使い方を学んでいます。前回は、書籍登録の処理を作ってみました。今回は登録した書籍を一覧で閲覧できるように取得機能を作ってみたいと思います。

準備

/pages/index.tsxのファイルを使って作成します。これまでの記事であれこれ書いてきましたが、その内容は1度リセットします。

$ tree ./pages/
./pages/
├── app.css
├── _app.tsx
├── book
│   └── resister.tsx
└── index.tsx

画面UIの作成

まずは画面UIから作成します。今回は書籍件数に応じて動的に表示する部分がありますが全体的なレイアウトを決めるため、静的な形で作成します。

サンプルコードはこちら

"use client";

export default function Home() {
  return (
    <div className="space-y-5">
      <h3 className="text-3xl font-bold dark:text-white">書籍一覧</h3>
      <div>
        <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
          書籍を登録する
        </button>
      </div>
      <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>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td className="border border-slate-300 p-2">xxxxxxxxxxxxxxxxxxxxx</td>
              <td className="border border-slate-300 p-2">9999999999999</td>
              <td className="border border-slate-300 p-2">これから始める人のためのDevOps超入門</td>
              <td className="border border-slate-300 p-2">田中 太郎</td>
              <td className="border border-slate-300 p-2">日本仮想化技術出版</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  );
}

コードを実行したときに表示される画面はこんな感じです。

取得処理の作成

画面UIの準備ができたら次は取得処理を実装してみましょう。GraphQLでは取得方法はgetlistの2パターンあります。前者のgetは、IDを指定して1件のデータを取得できます。後者のlistは、複数件のデータをまとめて取得できます。今回は一覧表示をしたいので、listを使って表示してみます。

fetchBooksという関数を1つ作ります。関数の中ではGraphQLを通じてDynamoDBにデータを取得する処理を記述しています。

./pages/fetchBooks.tsというファイルを作成して、次のコードを貼り付けます。

import { Book, ListBooksQuery } from "@/src/API";
import { listBooks } from "@/src/graphql/queries";
import { GraphQLResult, generateClient } from "aws-amplify/api";

export default async function fetchBooks() {
  const client = generateClient();
  const response = await client.graphql({
    query: listBooks,
  }) as GraphQLResult<ListBooksQuery>;

  if (response.errors) {
    throw new Error(response.errors[0].message);
  }

  if (!response.data?.listBooks) {
    throw new Error("Book not created");
  }

  const books: Book[] = response.data?.listBooks.items
    .filter((item): item is NonNullable<typeof item> => !!item)
    .map((item) => item);
  return books;
}

メインの処理はこの部分ですが、データ登録時と部分的に少し変わっただけで、基本的なフォーマットは同じです。

  const client = generateClient();
  const response = await client.graphql({
    query: listBooks,
  }) as GraphQLResult<ListBooksQuery>;

それ以降の処理に関しては、レスポンス内容を取り扱いしやすいようにあれこれ加工しているところになります。今回はそこまで重要な処理ではないため、省略します。

データ取得するための関数の準備ができましたが、今回はカスタムフックというものを作ります。データ操作系をまとめておくとスッキリするので、筆者的にはこのような形でまとめています。loadingなどを用意しておくと画面描画時に読み込み中のアイコンを表示したりする際に活用できます。

hooks/useBook.tsというファイルを作成して、次のコードを貼り付けてください。

import fetchBooks from "@/pages/fetchBooks";
import { Book } from "@/src/API";
import { useEffect, useState } from "react";

export default function useBook() {
  const [books, setBooks] = useState<Book[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetchBooks()
      .then((books) => {
        setBooks(books);
      })
      .catch((error) => {
        setError(error);
      }).finally(() => {
        setLoading(false);
      });
  }, []);

  return { books, loading, error };
}

useEffectはこれまでの記事で触れていないですがざっくりと言ってしまうと、指定した値が変更される度に引数で渡した関数を実行してくれるものです。useStateと組み合わせてよく使用されます。

最後に作成したカスタムフックを./pages/index.tsxで呼び出してみましょう。

"use client";

+ import useBook from "@/hooks/useBook";
+ import Link from "next/link";

export default function Home() {
+  const { books, loading, error } = useBook();

+ if (loading) {
+   return <div>データを読み込んでいます...</div>;
+ }

+ if (error) {
+   return <div>エラーが発生しました</div>;
+ }

  return (
    <div className="space-y-5">
      <h3 className="text-3xl font-bold dark:text-white">書籍一覧</h3>
      <div>
        <Link href="/book/resister" className="text-blue-500 hover:text-blue-700">
          書籍を登録する
        </Link>

      </div>
      <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>
            </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>
+             </tr>
+           ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

カスタムフックを使いときは、const { ... } = useBooks();のような形で呼び出します。booksには取得できた書籍情報の一覧が格納されており、loadingに現在処理中かどうかのステータス保持されています。

書籍一覧を表示するときには{books.map((book) => ( ... ))}のような形でループして1件ずつ表示すると次の画像のように表示されます。

データ取得編はこれで以上です。

おわりに

前回作成したデータを表示できるようにするために、今回はデータを取得する処理を実装しました。これで、作成したデータを確認することができるようになりました。次回は、編集編をお届けします。