普段バックエンドでPHP/Laravelを扱っている私が、現在Next.jsを遊びながら学習中です。
前回の記事で、Next.jsとLaravel APIとの連携をすることができました。しかし、対象データがたった3件だったので現実にはそぐいません。
……ということで、もっと大量のデータを扱うとどうなるのかというのを、実験してみよう!というのが今回の趣旨です。
そのため、本ページの読者対象は、私と同じくLaravelは理解があるけど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
マイグレーションファイルの編集
生成されたマイグレーションファイルを編集して、テーブルの構造を定義します。ここではname
と description
カラムを追加しました。
<?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
ですので動作確認してみます。
前回からの改修点
全件ロードしていたのが改善され、適切にページング処理がされていますね。
また、前回はロード時に「結果が見つかりませんでした。」となり、その後送れて結果がでました。
それが不自然だったため、ロード時はLoading…に変更。 本当に結果が無い時だけ「結果が見つかりませんでした。」としています。
課題
ページング処理も問題無く、これでいいなと思ったのもつかの間。2ページ目、3ページ目と移動しても、URLが/items
と変わっていないことに気がつきました。
これでは2ページ目以降に直接リンクを張れません。
普段は大抵のことなら解決できるChatGPTに聞いても、イマイチな回答しか得られず……(途中でPages Router前提になっていたり)。まだまだ情報が安定していないのかもしれません。
そのため、これについては次の課題にしたいと思います。
とは言え今回触っていて、道が開けてきた気がします。
まとめ
実際のWebアプリケーションに近い処理を試してみることにより、Next.jsで表示するだけなら実現もできることがわかりました。
後はCSSでデザインwの整えればそれなりに見えることでしょう。今回はChatGPTに手助けしてもらいましたが、JS/TypeScriptが分かればわりとすんなり入ってくるコードです。
あとは認証・認可などができるようになれば、仕事の幅が広がってきそうです!フロントエンドは独特の面白さがあるなぁと、はまりそうな予感です。
コメント