Reactで作った検索機能つき一覧画面に自作のページャを追加してみた

目次

はじめに

「一覧画面にページャをつけたいんだけど、ライブラリ入れるのはめんどくさい・・・」
「ライブラリ入れるのはいいんだけど、見た目とかカスタマイズしたいんだよな・・・」
「かといってライブラリのカスタマイズはちょっとハードル高いし・・・」

ページャの実装でお悩みのそこのあなた。
この記事を読んで、ページャをさくっと作れるようになりましょう。

実物はこちら

あわせて読みたい
React Recipe Reactアプリのレシピ集

リポジトリはこちら

GitHub
GitHub - hakoratory/react-recipe Contribute to hakoratory/react-recipe development by creating an account on GitHub.

概要

この記事を読むことで、以下のような画面が作れるようになります。

以下の仕様を想定して作成しています。

  • 初期表示時は検索された状態
  • 1ページあたりの表示件数は2件(データ作るのがめんどくさかったです、すみません)
  • ページャの数字ボタンが押されたら、押された数字のページを表示する
  • ページャの矢印ボタンは一つ前のページ、一つ後のページを表示する
  • 検索ボタンが押されたら、表示ページをリセットして1ページ目を表示する

実装

先にコード全容です。

'use client'

import '../style.css'
import {Box, Button} from "@mui/material";
import {Input} from "@/components/input";
import {useTextInput} from "@/hooks/use-text-input";
import {tableUsers} from "@/data/data";
import {
  FetchTableUsersWithPageRequestType,
  FetchTableUsersWithPageResponseType,
  PagerProps,
  TableUser
} from "@/types/table";
import {CSSProperties, useEffect, useState} from "react";

const fetchUsers = async (param: FetchTableUsersWithPageRequestType): Promise<FetchTableUsersWithPageResponseType> => {
  if (process.env.NEXT_PUBLIC_USE_MOCK_DATA === 'true') {
    const filteredUsers = tableUsers.filter(user => user.name.indexOf(param.name ?? '') > -1)
    const from = (param.page - 1) * param.pageSize
    const to = param.page * param.pageSize
    return {
      users: filteredUsers.slice(from, to),
      totalCount: tableUsers.length,
    }
  } else {
    const response = await fetch('/api/table/users-with-page', {
      method: 'POST',
      body: JSON.stringify(param)
    })
    return await response.json()
  }
}

const Pager = ({totalCount, page, pageSize, onPageChange}: PagerProps) => {
  const totalPageCount = Math.ceil(totalCount / pageSize)
  const pageButtonStyle: CSSProperties = {
    border: '1px solid #000',
    width: '30px',
    textAlign: 'center'
  }
  return (
    <div style={{display: 'flex', justifyContent: 'flex-end', marginTop: '1rem'}}>
      {
        // ページ数が2ページ以上のとき、「<」ボタンを表示する
        totalPageCount > 1 &&
        <div
          style={{
            ...pageButtonStyle,
            backgroundColor: page <= 1 ? '#dcdcdc' : 'transparent',
          }}
          onClick={() => {
            if (page <= 1) return
            onPageChange(page - 1)
          }}
        >
          {'<'}
        </div>
      }
      {
        [...Array(totalPageCount)].map((_, index) => (
          index + 1 === page
            ? (
              <div key={index}
                   style={{
                     ...pageButtonStyle,
                     marginLeft: '8px',
                     backgroundColor: '#f0f8ff'
                   }}
              >
                {index + 1}
              </div>
            )
            : (
              <div key={index}
                   style={{
                     ...pageButtonStyle,
                     marginLeft: '8px'
                   }}
                   onClick={() => onPageChange(index + 1)}
              >
                {index + 1}
              </div>
            )
        ))
      }
      {
        // ページ数が2ページ以上のとき、「>」ボタンを表示する
        totalPageCount > 1 &&
        <div
          style={{
            ...pageButtonStyle,
            marginLeft: '8px',
            backgroundColor: page >= totalPageCount ? '#dcdcdc' : 'transparent',
          }}
          onClick={() => {
            if (page >= totalPageCount) return
            onPageChange(page + 1)
          }}
        >
          {'>'}
        </div>
      }
    </div>
  )
}

export default function TableScratchWithPager() {
  const [searchWord, setSearchWord] = useTextInput()
  const [tableUserData, setTableUserData] = useState<Array<TableUser>>([])
  const [totalCount, setTotalCount] = useState<number>(0)
  const [currentPage, setCurrentPage] = useState<number>(1)

  useEffect(() => {
    (async () => {
      const newUsers = await fetchUsers({name: null, page: 1, pageSize: 2})
      setTableUserData(newUsers.users)
      setTotalCount(newUsers.totalCount)
      setCurrentPage(1)
    })()
  }, [])

  const handleSearch = async (newPage: number) => {
    const newUsers = await fetchUsers({name: searchWord.value, page: newPage, pageSize: 2})
    setTableUserData(newUsers.users)
    setTotalCount(newUsers.totalCount)
    setCurrentPage(newPage)
  }

  return (
    <main>
      <Box m={2}>
        <Box display="flex">
          <Box>
            <Input label="名前" errors={searchWord.errors} onChange={(e) => setSearchWord(e.target.value)}/>
          </Box>
          <Box ml={3} pt={3}>
            <Button
              type="button"
              variant="contained"
              disabled={searchWord.errors.length > 0}
              onClick={() => handleSearch(1)}
            >
              検索
            </Button>
          </Box>
        </Box>
      </Box>
      <Box m={2}>
        <table>
          <thead>
          <tr>
            <th>名前</th>
            <th>所属</th>
            <th>好きなもの</th>
          </tr>
          </thead>
          <tbody>
          {
            tableUserData.map((user) => (
              <tr key={user.id}>
                <td>{user.name}</td>
                <td>{user.department}</td>
                <td>{user.favoriteThings}</td>
              </tr>
            ))
          }
          </tbody>
        </table>
        <Pager
          totalCount={totalCount}
          page={currentPage}
          pageSize={2}
          onPageChange={handleSearch}
        />
      </Box>
    </main>
  )
}

各部分の解説

APIをページ指定できるよう改修

UIはもちろんですが、APIもページ指定ができるようにする必要があります。

今回は TypeScript のメモリ上で配列をあれこれしていますが、実際にはSQLの offset と limit でやると思います。

import {NextRequest, NextResponse} from "next/server"
import {tableUsers} from "@/data/data";
import {FetchTableUsersWithPageRequestType, TableUser} from "@/types/table";

export async function POST(request: NextRequest): Promise<NextResponse> {
  const params: FetchTableUsersWithPageRequestType = await request.json()
  const filteredUsers: TableUser[] = tableUsers.filter(user => user.name.indexOf(params.name ?? '') > -1)
  const from = (params.page - 1) * params.pageSize
  const to = params.page * params.pageSize
  return NextResponse.json({
    users: filteredUsers.slice(from, to),
    totalCount: filteredUsers.length
  })
}

リクエストとレスポンスも調整が必要です。
リクエストには、表示したいページ page 、1ページあたりの表示件数 pageSize を追加します。
レスポンスには、検索でヒットした総件数 totalCount を追加します。

export type FetchTableUsersWithPageRequestType = {
  name: string | null,
  page: number,
  pageSize: number
}

export type FetchTableUsersWithPageResponseType = {
  users: TableUser[],
  totalCount: number
}

ページャコンポーネント

今回の肝となるページャコンポーネントです。

以下の引数が定義されていることを確認してください。

  • totalCount: 検索でヒットした総件数
  • page: 表示するページ
  • pageSize: 1ページあたりの表示件数
  • onPageChange: ページ変更時の処理

ポイントとなるのは、2行目で totalCountpageSize から総ページ数 totalPageCount を算出しているところです。
ここで算出した totalPageCount を含めた4つの値(totalCountpagepageSizetotalPageCount)を使うことで、ページャのUIを実現することができます。

const Pager = ({totalCount, page, pageSize, onPageChange}: PagerProps) => {
  const totalPageCount = Math.ceil(totalCount / pageSize)
  const pageButtonStyle: CSSProperties = {
    border: '1px solid #000',
    width: '30px',
    textAlign: 'center'
  }
  return (
    <div style={{display: 'flex', justifyContent: 'flex-end', marginTop: '1rem'}}>
      {
        // ページ数が2ページ以上のとき、「<」ボタンを表示する
        totalPageCount > 1 &&
        <div
          style={{
            ...pageButtonStyle,
            backgroundColor: page <= 1 ? '#dcdcdc' : 'transparent',
          }}
          onClick={() => {
            if (page <= 1) return
            onPageChange(page - 1)
          }}
        >
          {'<'}
        </div>
      }
      {
        [...Array(totalPageCount)].map((_, index) => (
          index + 1 === page
            ? (
              <div key={index}
                   style={{
                     ...pageButtonStyle,
                     marginLeft: '8px',
                     backgroundColor: '#f0f8ff'
                   }}
              >
                {index + 1}
              </div>
            )
            : (
              <div key={index}
                   style={{
                     ...pageButtonStyle,
                     marginLeft: '8px'
                   }}
                   onClick={() => onPageChange(index + 1)}
              >
                {index + 1}
              </div>
            )
        ))
      }
      {
        // ページ数が2ページ以上のとき、「>」ボタンを表示する
        totalPageCount > 1 &&
        <div
          style={{
            ...pageButtonStyle,
            marginLeft: '8px',
            backgroundColor: page >= totalPageCount ? '#dcdcdc' : 'transparent',
          }}
          onClick={() => {
            if (page >= totalPageCount) return
            onPageChange(page + 1)
          }}
        >
          {'>'}
        </div>
      }
    </div>
  )
}

表示しているページによっては、スタイルや処理分岐をしている点に注意してください。

例えば、1ページ目を表示しているのに< ボタンが押せてしまうと、「0ページ目を表示する」というよくわからないことになってしまいますよね。
あとは、2ページ目を表示しているときに2 ボタンが押せてしまうと、無駄な検索処理が発生してしまいます。

ページャコンポーネントを使う

ページャコンポーネントができたので、早速使ってみましょう。
コンポーネント化しているので、必要なパラメータを渡すのみです。

        <table>
          <thead>
          <tr>
            <th>名前</th>
            <th>所属</th>
            <th>好きなもの</th>
          </tr>
          </thead>
          <tbody>
          {
            tableUserData.map((user) => (
              <tr key={user.id}>
                <td>{user.name}</td>
                <td>{user.department}</td>
                <td>{user.favoriteThings}</td>
              </tr>
            ))
          }
          </tbody>
        </table>
        <Pager
          totalCount={totalCount}
          page={currentPage}
          pageSize={2}
          onPageChange={handleSearch}
        />
      </Box>
    </main>
  )
}

おわりに

いかがだったでしょうか?
今回は、3大めんどくさい UI の一つ、ページャを実装してみました。

より丁寧に作るのであれば、ページ数が多いとき 「1, 2, 3, … 20」というように省略表示したりなどが考えられますね。
あとは、先頭ページと最終ページを表示するボタンもあると便利かもしれません。
いずれの処理も、今回作成したコンポーネントをベースに作ることができると思います。
ぜひ、作ってみてくださいね。

ハコラトリは、Reactを習得したい駆け出しエンジニアを応援しています。

Reactレシピでは、これからもReactで色々なUIを作って紹介していきますので、「こんなUIを作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

28歳のときに音楽家からエンジニアに転向。
WEB系のシステム開発(Java, C#, Vue, React など)に携わっています。

自分らしく力を発揮できて、自信を持って生きられる人を増やすための活動をしています。

コメント

コメントする

目次