Next.js初心者がLaravel API連携で大量データの表示&ページング処理してみる

Laravel Next.js×API 大量データの表示 とページング処理 Laravel
※当サイトはアフィリエイト広告を掲載しています。

普段バックエンドでPHP/Laravelを扱っている私が、現在Next.jsを遊びながら学習中です。

前回の記事で、Next.jsとLaravel APIとの連携をすることができました。しかし、対象データがたった3件だったので現実にはそぐいません。

……ということで、もっと大量のデータを扱うとどうなるのかというのを、実験してみよう!というのが今回の趣旨です。

そのため、本ページの読者対象は、私と同じくLaravelは理解があるけどNext.jsが初心者・あるいはほぼ分からない人向けです。少しでも参考になるところがあれば幸いです。

「試行錯誤の仮定や結果」を参考にしていただくものであり、「正解」を紹介するためのページではないことをご承知おき下さい。

また、データは前回のNext.jsインストールの記事からそのまま引き継いで作業しています。同じように作りたい方はそちらの記事もご参考ください。

LaravelのSeederでデータを増やす

さて、前回のAPIでは連携するデータ対象が3件しかなく、しかも配列で用意していました。

テスト用にもっと大量のデータが欲しいです。

こういったダミーデータを生成するにはいくつかの方法があると思いますが、Laravelならすぐ思いつくのはSeederを利用する方法です。

普段あまり使う機会が無かったので、これ幸いと思いSeederを利用することにしました。

MySQLでDB作成

まだLarvelにデータベースの設定すらしていなかったので行っておきます。

まずは専用のデータベースをutf8mb4_unicode_ciで作成。私はphpMyAdminで以下の様に行いました。もちろんコマンドラインから行ってもかまいません。

Laravelに設定

.envファイルを修正します。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laranext
DB_USERNAME=usename
DB_PASSWORD=password

itemsテーブルの用意

今回はitemsテーブルを作成したいと思います。

そのためにマイグレーションファイルを作成します。……が、どうせ後で使うので、モデルとコントローラーも一括で作成してしまおうと思います。

マイグレーション、モデル、コントローラーの一括生成

コマンドラインで以下のコマンドを実行し、マイグレーションファイル、モデル、コントローラーをまとめて作成します。

php artisan make:model Item -m -c

これにより、以下が同時に生成されます。便利すぎです。

  • マイグレーション:
    database/migrations/xxxx_xx_xx_xxxxxx_create_items_table.php
  • モデル:
    app/Models/Item.php
  • コントローラー:
    app/Http/Controllers/ItemController.php

マイグレーションのファイル名は、実際には日付を意味する数字が入ります。

マイグレーションファイルの編集

生成されたマイグレーションファイルを編集して、テーブルの構造を定義します。ここではnamedescription カラムを追加しました。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('items', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->text('description');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('items');
    }
};

マイグレーションの実行

以下のコマンドを実行してマイグレーションを実行し、データベースにitems テーブルを作成します。

php artisan migrate

Seederでデータの挿入

作成したitemsテーブルに、ダミーデータを挿入します。

Seederの作成

大量のダミーデータを生成するためのシーダーを作成します。以下のコマンドです。

php artisan make:seeder ItemsTableSeeder

これで、database\seeders\ItemsTableSeeder.phpが生成されます。

Seederの編集

生成したファイルを、以下の様に編集します。LaravelのFakerライブラリを使用して、ランダムな日本語のデータを生成しています。

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Item; // モデルを使用

class ItemsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // 日本語のデータ
        $faker = \Faker\Factory::create('ja_JP'); 

        for ($i = 0; $i < 5000; $i++) {
            Item::create([
                'name' => $faker->name,
                'description' => $faker->realText(30)
            ]);
        }
    }
}

19行目の5000 という数字は生成数ですので、状況に応じて変更してください。

シーダーの実行

以下のコマンドで、実行します。

php artisan db:seed --class=ItemsTableSeeder

これでデータベースに値が入りました。

phpMyAdminで確認すると、以下の様な感じです。

Laravel側のAPIでのJSON出力

データができたため、次にLaravelからAPIでJSON出力できるようにします。

コントローラーの編集

コントローラー: app/Http/Controllers/ItemController.phpを編集し、データを取得するメソッドを追加します。

<?php

namespace App\Http\Controllers;

use App\Models\Item;

class ItemController extends Controller
{
    public function index()
    {
      // 1ページあたりの表示件数
        $items = Item::paginate(5); 
        return response()->json($items);
    }
}

前回の記事は全件取得していましたが、今回は大量データということもあり、ひとまず5件ずつの取得(12行目)としました。

たった2行で実現できるなんて、本当に素敵です。

ルートの設定

routes/api.php ファイルを開き、以下の様な行を追加してルートを設定します。

use App\Http\Controllers\ItemController;

Route::get('/items', [ItemController::class, 'index']);

これで、私の場合はhttp://localhost:8000/api/itemsでアクセスできる状態です。

以下の様になればOKです。

Next.jsで実装する

やっとNext.jsで実装するところまできました。本題はここからです。今回は大量のページが存在するため、Next.jsでページング処理をする必要があります。

コード

Next.jsのプロジェクト内に/app/items/page.tsxというファイルを作成し、以下の様にします。

'use client';
import React, { useState, useEffect } from 'react';

interface Item {
    id: number;
    name: string;
    description: string;
}

const ItemList: React.FC = () => {
    const [items, setItems] = useState<Item[]>([]);
    const [currentPage, setCurrentPage] = useState(1);
    const [totalPages, setTotalPages] = useState(0);
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
        setIsLoading(true);
        fetch(`http://localhost:8000/api/items?page=${currentPage}`)
            .then(response => response.json())
            .then(response => {
                setItems(response.data);
                setTotalPages(response.last_page);
                setIsLoading(false);
            })
            .catch(error => {
                console.error('Error fetching items:', error);
                setIsLoading(false);
            });
    }, [currentPage]);

    const setPage = (page: number) => {
        setCurrentPage(page);
    };

    const pagerNumbers = [];
    const pagesToShow = 5;
    let startPage = currentPage - 2;
    let endPage = currentPage + 2;

    if (startPage <= 0) {
        endPage -= (startPage - 1);
        startPage = 1;
    }
    if (endPage > totalPages) {
        endPage = totalPages;
    }

    for (let i = startPage; i <= endPage; i++) {
        pagerNumbers.push(
            <button key={i} onClick={() => setPage(i)} disabled={currentPage === i}>
                {i}
            </button>
        );
    }

    return (
        <div>
            <h1>Item List</h1>
            {isLoading ? <p>Loading...</p> : items.length > 0 ? (
                <>
                    {items.map((item) => (
                        <div key={item.id}>
                            <h3>ID: {item.id} {item.name}</h3>
                            <p>{item.description}</p>
                        </div>
                    ))}
                    <div>
                        <p>Page {currentPage} of {totalPages}</p>
                        <button onClick={() => setPage(currentPage - 1)} disabled={currentPage === 1}><</button>
                        {pagerNumbers}
                        <button onClick={() => setPage(currentPage + 1)} disabled={currentPage === totalPages}>></button>
                    </div>
                </>
            ) : (
                <p>結果が見つかりませんでした。</p>
            )}
        </div>
    );
};

export default ItemList;

動作確認

これで実現できました!/app/items/page.tsxに対応するURLは私の場合http://localhost:3000/itemsですので動作確認してみます。

余談ですが、Next.jsのApp Routerは、/app/ディレクトリ内のpage.tsxpage.jsxという名称のファイルがページ内容になります。

前回からの改修点

全件ロードしていたのが改善され、適切にページング処理がされていますね。

また、前回はロード時に「結果が見つかりませんでした。」となり、その後送れて結果がでました。

それが不自然だったため、ロード時はLoading…に変更。 本当に結果が無い時だけ「結果が見つかりませんでした。」としています。

実際にはロード時はローディングアニメーションを表示させることが多いとは思います。

課題

ページング処理も問題無く、これでいいなと思ったのもつかの間。2ページ目、3ページ目と移動しても、URLが/itemsと変わっていないことに気がつきました。

これでは2ページ目以降に直接リンクを張れません。

普段は大抵のことなら解決できるChatGPTに聞いても、イマイチな回答しか得られず……(途中でPages Router前提になっていたり)。まだまだ情報が安定していないのかもしれません。

そのため、これについては次の課題にしたいと思います。

Lara
Lara

とは言え今回触っていて、道が開けてきた気がします。

App Routerの正式版は2023年5月~とのことですが、現在のChatGPTはは2023年4月までの情報のようです。ChatGPTに助けてもらうにはPages Routerを使った方が良いのかもしれません。

まとめ

実際のWebアプリケーションに近い処理を試してみることにより、Next.jsで表示するだけなら実現もできることがわかりました。

後はCSSでデザインwの整えればそれなりに見えることでしょう。今回はChatGPTに手助けしてもらいましたが、JS/TypeScriptが分かればわりとすんなり入ってくるコードです。

あとは認証・認可などができるようになれば、仕事の幅が広がってきそうです!フロントエンドは独特の面白さがあるなぁと、はまりそうな予感です。

コメント

タイトルとURLをコピーしました