前回は、バックエンドでページネーションを追加しました。

今回は、フロントエンドでページネーションを実装します。

ソースコードは、こちらです。

バックエンド:

フロントエンド:

まずは、前回バックエンドで作成した、skipとtakeを実装します。

Apollo Studioで、BooksのGraphQLを作成します。

query FilteredBooks($skip: Int!, $take: Int!) {
    books(skip: $skip, take: $take) {
      id
      title
      author
      category {
        name
      }
      isRead
      createdAt
    }
  }

Bookボタンをクリックすると、

image2

正常にデータを取得することができます。

BooksのGraphQLをフロントエンドのbooksDataGraphQLと差し替えます。

const booksData = gql`
  query FilteredBooks($skip: Int!, $take: Int!) {
    books(skip: $skip, take: $take) {
      id
      title
      author
      category {
        name
      }
      isRead
      createdAt
    }
  }
`;

booksDatauseQueryvariableを指定しましょう。

const { data, error, loading } = useQuery(booksData, {
  variables: {
    skip: 2,
    take: 3,
  },
});

ブラウザで動作確認してみます。

image3

GraphQLが機能しています。

次にページネーションのUIを作成します。

ページネーションは、MUIを使います。

https://mui.com/components/pagination/ からControlled paginationページネーションを参考に、ページネーションを追加します。

import React, { useState } from "react";
import { gql, useMutation, useQuery } from "@apollo/client";

import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Title from "../components/Title";
import { Box, Button, Link, Pagination, Stack, styled } from "@mui/material";
import ModalBook from "../components/ModalBook";

type TypeBook = {
  id: number;
  title: string;
  author: string;
  category: {
    name: string;
  };
  isRead: boolean;
  createdAt: string;
};

const booksData = gql`
  query FilteredBooks($skip: Int!, $take: Int!) {
    books(skip: $skip, take: $take) {
      id
      title
      author
      category {
        name
      }
      isRead
      createdAt
    }
  }
`;

const updateBookData = gql`
  mutation UpdateBook($updateBookId: Int!, $input: UpdateBookInput!) {
    updateBook(id: $updateBookId, input: $input) {
      errors {
        message
      }
      book {
        title
      }
    }
  }
`;

export default function Book() {
  const [page, setPage] = useState(1);

  const { data, error, loading } = useQuery(booksData, {
    variables: {
      skip: 2,
      take: 3,
    },
  });

  const [updateBook] = useMutation(updateBookData);

  if (error) return <div>データを取得することができませんでした</div>;

  if (loading) return <div>Loading...</div>;

  const { books } = data;

  const handleChange = (event: React.ChangeEvent<unknown>, value: number) => {
    setPage(value);
  };

  return (
    <React.Fragment>
      <CBox>
        <Title>読書リスト</Title>
        <ModalBook />
      </CBox>
      <Table size="small">
        <TableHead>
          <TableRow>
            <TableCell>ID</TableCell>
            <TableCell>タイトル</TableCell>
            <TableCell>著者</TableCell>
            <TableCell>カテゴリ</TableCell>
            <TableCell>読了</TableCell>
            <TableCell>作成日</TableCell>
            <TableCell></TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {books.map((book: TypeBook) => (
            <TableRow key={book.id}>
              <TableCell>{book.id}</TableCell>
              <TableCell>{book.title}</TableCell>
              <TableCell>{book.author}</TableCell>
              <TableCell>{book.category.name}</TableCell>
              {book.isRead ? (
                <TableCell>
                  <Button
                    size="small"
                    onClick={() => {
                      updateBook({
                        variables: {
                          updateBookId: book.id,
                          input: {
                            isRead: false,
                          },
                        },
                      });
                    }}
                  >
                    済
                  </Button>
                </TableCell>
              ) : (
                <TableCell>
                  <Button
                    size="small"
                    onClick={() => {
                      updateBook({
                        variables: {
                          updateBookId: book.id,
                          input: {
                            isRead: true,
                          },
                        },
                      });
                    }}
                  >
                    未
                  </Button>
                </TableCell>
              )}
              <TableCell>
                {`${new Date(Number(book.createdAt))}`
                  .split(" ")
                  .splice(1, 3)
                  .join(" ")}
              </TableCell>
              <TableCell>
                <Link href={`/book/${book.id}`} underline="hover">
                  詳細画面
                </Link>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      <Stack spacing={2}>
        <Pagination
          count={10}
          page={page}
          color="primary"
          onChange={handleChange}
        />
      </Stack>
    </React.Fragment>
  );
}

const CBox = styled(Box)({
  display: "flex",
  justifyContent: "space-between",
});

では、ページネーションが表示されるか、ブラウザで確認しましょう。

image4

ページネーションが表示されました。

ページネーションが機能するようにします。

booksデータは、今のところ6件あるので、3件表示してボタンをクリックすると、次の3件が表示できるようにします。

Paginationのcountは、全体の数から表示されている数を割った数にします。

また、Paginationのcountが小数になることも考えられるので、小数になった場合、切り上げるようにします。

まずは、bookの全体数を取得します。

バックエンドに戻って、bookの全体数に関するスキーマとリゾルバを作成しましょう。

スキーマを作成するため、schema.tsを開きます。

typeDefsQuerytotalBooksを追加します。

type Query {
  totalBooks: Int!
  books(isRead: Boolean, skip: Int, take: Int): [Book!]!
  book(id: ID!): Book
  categories: [Category!]!
  category(id: ID!): Category
}

次は、リゾルバを作成します。

resolversフォルダのQuery.tsを開きます。

Query内に、totalBooksを作成します。

booksの全体数は、prismacountを使うことで取得することができます。

totalBooks: (_: any, __: any, { prisma }: Context) => {
  return prisma.book.count();
},

では、Apollo Studioで動作確認しましょう。

query TotalBooks {
  totalBooks
}

image5

TotalBooksボタンをクリックすると、

image6

booksの全体数を取得することができました。

booksの全体数を取得することができたので、フロントエンドのBook.tsxに戻ります。

totalBooksgqlを作成しましょう。

const totalBooks = gql`
  query TotalBooks {
    totalBooks
  }
`;

useQuerytotalBooksのデータを取得します。

const { data, error, loading } = useQuery(totalBooks);

すると、All destructured elements are unused.というエラーが発生します。

image7

これは、totalBooksのdataやerror、loadingとbooksDataのdataやerror、loadingが被っているため、エラーになります。

エラーを解消するために、それぞれののdataやerror、loadingに名前をつけてあげます。

const { data: totalBooksData, error: totalBooksError, loading: totalBooksLoaring } = useQuery(totalBooks);

  const { data: getBooksData, error: booksDataError, loading: booksDataLoading } = useQuery(booksData, {
    variables: {
      skip: 2,
      take: 3
    }
  });
if ( booksDataError)
  return <div>データを取得することができませんでした</div>;

if ( booksDataLoading) return <div>Loading...</div>;

const { books } = getBooksData;

これで、エラーを解消することができました。

一度にどのくらいデータを取得するか設定します。

const takeBooks = 3;

全体で何ページになるか設定します。

Paginationのcountは、全体の数から表示されている数を割った数にしたいので、totalBooksData.totalBookstakeBooksで割ります。

また、小数点は、切り上げるようにします。

const totalPages = Math.ceil(totalBooksData.totalBooks / takeBooks);

totalPagesをPaginationのcountに指定しましょう。

<Pagination
  count={totalPages}
  page={page}
  color="primary"
  onChange={handleChange}
/>

booksDatauseQueryで設定しているvariablesを修正します。

takeは、takeBooksを指定します。

また、skipは、現在のページ数から1を引いて、takeの数を掛けます。

例えば、現在のページが2ページ目で、取得するデータが3件だとすると、

(2 - 1) * 3 = 3となります。

const { data: getBooksData, error: booksDataError, loading: booksDataLoading } = useQuery(booksData, {
  variables: {
    skip: (page - 1) * takeBooks,
    take: takeBooks
  }
});

totalBooksErrortotalBooksLoaringも設定しましょう。

if (totalBooksError || booksDataError)
  return <div>データを取得することができませんでした</div>;

if (totalBooksLoaring || booksDataLoading) return <div>Loading...</div>;

一通り完了したので、動作確認します。

まずは、初めのページを確認すると、

image8

3件分表示されています。

また、ページネーションも最大2ページまでとなっています。

では、ページネーションの『2』をクリックすると、

image9

次の3件分データを取得すことができました。

ページネーションの『1』をクリックすると、

image10

初めの3件分のデータを取得することができました。

ソースコードは、こちらです。

バックエンド:

フロントエンド:

ブログ一覧