LaravelとReactでページネーションを実装する

LaravelとReactを使ってページネーション機能を初めて実装したのでメモしておきます。

Laravelページネーションの仕様確認

ページネーションの一番簡単な実装方法は、クエリビルダーまたは Eloquentのpaginateメソッドを使うことです。

// クエリビルダーを使う場合
use Illuminate\Support\Facades\DB;

$users = DB::table('users')->paginate(15)
// Eloquentを使う場合
use App\Models\User;

$users = User::paginate(15);

また、メソッドは下記の通り3種類ありますが、今回はpaginateメソッドを使っていきます。

メソッド 返されるインスタンス
paginate Illuminate\Pagination\LengthAwarePaginator
simplePaginate Illuminate\Pagination\Paginator
cursorPaginate Illuminate\Pagination\CursorPaginator

返される値を確認

$usersPaginator = User::paginate(5);
dd($usersPaginator);
Illuminate\Pagination\LengthAwarePaginator {
  #items: Illuminate\Database\Eloquent\Collection {#1353 ▶}
  #perPage: 5
  #currentPage: 1
  #path: "http://localhost:8000/users"
  #query: []
  #fragment: null
  #pageName: "page"
  +onEachSide: 3
  #options: array:2 [▶]
  #total: 111
  #lastPage: 23
}

自動でJSON形式に変換される

paginatorインスタンスは、ルーティングやコントローラーからreturnするだけでJSON形式に自動的に変換されます。(paginatorクラスがIlluminate\Contracts\Support\Jsonableを実装するため)

{
   "total": 50,
   "per_page": 15,
   "current_page": 1,
   "last_page": 4,
   "first_page_url": "http://laravel.app?page=1",
   "last_page_url": "http://laravel.app?page=4",
   "next_page_url": "http://laravel.app?page=2",
   "prev_page_url": null,
   "path": "http://laravel.app",
   "from": 1,
   "to": 15,
   "data":[
        {
            // Record...
        },
        {
            // Record...
        }
   ]
}

引用: Database: Pagination - Laravel 10.x - The PHP Framework For Web Artisans

(ドキュメントには書いてありませんでしたが、上記に加えてlinksという配列も含まれます。)

Laravel側の実装

さて、上述の通りLaravelからページネーションに必要なデータがJSON形式で返ってくることがわかったので、Reactへpropsで渡せば自由に使えそうです。

usersPaginatorという名前で上記JSONオブジェクトをReactに渡すこととします。

User一覧ページに表示することにします。

// routes/web.php
Route::get('users', [UserController::class, 'index'])->name('users.index');

コントローラの実装。ここではInertiaを使っています。

// app/Http/Controllers/UserController.php
use App\Models\User;
use Inertia\Inertia;

public function index()
{
    $usersPaginator = User::paginate(20);

    return Inertia::render('User/Index', [
        'usersPaginator' => $usersPaginator,
    ]);
}

Reactにページネーション実装

Paginationコンポーネント作成

親コンポーネントからpropsとして渡されるpaginatorオブジェクトから、必要な情報を分割代入を使って変数として取り出します。

// resources/js/Components/Pagination.jsx
export default function Pagination({ paginator }) {
  const {
    first_page_url,
    last_page_url,
    next_page_url,
    prev_page_url,
    current_page,
    last_page
  } = paginator;

  return (
    <div className="pagination">
      <a
        href={first_page_url}
        className="pagination-link"
        aria-disabled={current_page === 1}
      >
        最初
      </a>

      <a
        href={prev_page_url}
        className="pagination-link"
        aria-disabled={!prev_page_url}
      >
        前へ
      </a>

      <span className="current-page">
        {current_page} / {last_page}
      </span>

      <a
        href={next_page_url}
        className="pagination-link"
        aria-disabled={!next_page_url}
      >
        次へ
      </a>

      <a
        href={last_page_url}
        className="pagination-link"
        aria-disabled={current_page === last_page}
      >
        最後
      </a>
    </div>
  );
}

first_page_urlなどは該当のURLを持っているため、aタグのリンク先に指定するだけです。

// ページ遷移ボタン例
<a
  href={first_page_url}
  className="pagination-link"
  aria-disabled={current_page === 1}
>
  最初
</a>

aria-disabled属性にtrue/falseが入るように条件式を書いておき、trueのとき使用不可のスタイリングを書いておきます。

aria-disabled={current_page === 1}
aria-disabled={!prev_page_url}
aria-disabled={!next_page_url}
aria-disabled={current_page === last_page}
// pagination.scss
.pagination-link {
  &[aria-disabled="true"] {
    opacity: 0.5;
    pointer-events: none;
    cursor: not-allowed;
  }
}

親コンポーネント作成

親コンポーネントのUser/Index.jsxの適当な場所にPaginationコンポーネントを埋め込みます。

// resources/js/Pages/User/Index.jsx
import Pagination from "@/Components/Pagination";

export default function Index({ usersPaginator }) {

  return (
    <>  
      <Pagination paginator={usersPaginator} />
      ...
    </>
  );
}

件数を表示したい場合はtotalを使います。

{usersPaginator.total}件

usersPaginator.dataはUserオブジェクトを格納したコレクション(配列)を持っているため、mapを使って回せば一覧表示もできます。

<ul>
  {usersPaginator.data.map(user => (
    <li key={user.id}>
      {user.id}: {user.name}
    </li>
  ))}
</ul>

補足: Sassレイアウト

上記でコーディングしたページネーションコンポーネントのClassNameに対応したSassも適当に書いたので一応載せときます。

.pagination {
  align-items: center;
  display: flex;
  justify-content: center;
}

.pagination-link {
  background-color: #eee;
  border-radius: 4px;
  border: 1px solid #ddd;
  color: #333;
  padding: 4px 8px;
  transition: background-color 0.2s ease;

  &+.pagination-link {
    margin-left: 4px;
  }

  &[aria-disabled="true"] {
    opacity: 0.5;
    pointer-events: none;
    cursor: not-allowed;
  }

  &:hover {
    background-color: #ddd;
  }
}

.current-page {
  font-size: 0.9rem;
  margin: 0 4px;
}

以上で簡単ではありますがシンプルなページネーションが実装できました。Laravel返されるページネーションのJSONオブジェクトをReactで取り扱うだけなので、Laravel側のページネーションの仕様がわかっていれば簡単に実装できそうです。