株式会社WR

株式会社WR

WEB TOTAL CONSULTING

Laravel LivewireでインタラクティブなUIを作る——JavaScriptを最小限に
ブログ一覧へ
技術ブログ

Laravel LivewireでインタラクティブなUIを作る——JavaScriptを最小限に

Laravel LivewireはPHPだけでリアルタイム検索・フォームバリデーション・無限スクロールなどのインタラクティブなUIが実装できるライブラリです。Alpine.jsとの使い分けも解説します。

Laravel Livewireとは

Laravel Livewireは、PHPだけでインタラクティブなUIコンポーネントを作成できるフルスタックフレームワークです。通常、動的UIにはJavaScriptフレームワーク(Vue.js・React・Alpine.js)が必要ですが、Livewireを使うとPHPのクラスとBladeテンプレートだけでリアルタイムな画面更新が実現できます。

仕組みの概要

  1. ユーザーがフォームを入力・ボタンをクリック
  2. Livewireが差分をサーバーに送信(Ajax)
  3. PHPが処理してHTMLの差分を返す
  4. LivewireがDOM差分更新(ページ再読み込みなし)

インストール

composer require livewire/livewire

# スターターキット(Laravel Breeze + Livewire)
php artisan breeze:install livewire

最初のLivewireコンポーネント——リアルタイム検索

php artisan make:livewire ProductSearch
// app/Livewire/ProductSearch.php
<?php

namespace App\Livewire;

use App\Models\Product;
use Livewire\Component;
use Livewire\WithPagination;

class ProductSearch extends Component
{
    use WithPagination;

    public string $search    = '';
    public string $category  = '';
    public string $sortBy    = 'name';
    public string $sortOrder = 'asc';

    // プロパティが変更されたらページをリセット
    public function updatingSearch(): void
    {
        $this->resetPage();
    }

    public function updatingCategory(): void
    {
        $this->resetPage();
    }

    public function render()
    {
        $products = Product::query()
            ->when($this->search, fn($q) =>
                $q->where('name', 'like', "%{$this->search}%")
                  ->orWhere('description', 'like', "%{$this->search}%")
            )
            ->when($this->category, fn($q) =>
                $q->where('category', $this->category)
            )
            ->orderBy($this->sortBy, $this->sortOrder)
            ->paginate(20);

        $categories = Product::distinct()->pluck('category');

        return view('livewire.product-search', compact('products', 'categories'));
    }

    public function sort(string $field): void
    {
        if ($this->sortBy === $field) {
            $this->sortOrder = $this->sortOrder === 'asc' ? 'desc' : 'asc';
        } else {
            $this->sortBy    = $field;
            $this->sortOrder = 'asc';
        }
    }
}

Bladeテンプレート

{{-- resources/views/livewire/product-search.blade.php --}}
<div>
    {{-- 検索・フィルタ --}}
    <div class="flex flex-col sm:flex-row gap-4 mb-6">
        <div class="flex-1">
            <input
                wire:model.live.debounce.300ms="search"
                type="text"
                placeholder="商品名・説明で検索..."
                class="w-full px-4 py-2 border border-gray-300 rounded-lg
                       focus:ring-2 focus:ring-blue-500 focus:outline-none"
            >
        </div>
        <select
            wire:model.live="category"
            class="px-4 py-2 border border-gray-300 rounded-lg"
        >
            <option value="">全カテゴリ</option>
            @foreach($categories as $cat)
                <option value="{{ $cat }}">{{ $cat }}</option>
            @endforeach
        </select>
    </div>

    {{-- ローディングインジケーター --}}
    <div wire:loading class="text-center py-4 text-gray-500">
        <svg class="animate-spin h-6 w-6 mx-auto" ...></svg>
    </div>

    {{-- 商品テーブル --}}
    <div wire:loading.remove>
        <table class="w-full text-sm">
            <thead class="bg-gray-50">
                <tr>
                    <th class="text-left p-3 cursor-pointer hover:bg-gray-100"
                        wire:click="sort('name')">
                        商品名
                        @if($sortBy === 'name')
                            {{ $sortOrder === 'asc' ? '▲' : '▼' }}
                        @endif
                    </th>
                    <th class="text-right p-3 cursor-pointer hover:bg-gray-100"
                        wire:click="sort('price')">
                        価格
                        @if($sortBy === 'price')
                            {{ $sortOrder === 'asc' ? '▲' : '▼' }}
                        @endif
                    </th>
                    <th class="text-left p-3">カテゴリ</th>
                </tr>
            </thead>
            <tbody>
                @forelse($products as $product)
                    <tr class="border-t hover:bg-gray-50">
                        <td class="p-3">{{ $product->name }}</td>
                        <td class="p-3 text-right">¥{{ number_format($product->price) }}</td>
                        <td class="p-3">{{ $product->category }}</td>
                    </tr>
                @empty
                    <tr>
                        <td colspan="3" class="p-6 text-center text-gray-400">
                            商品が見つかりません
                        </td>
                    </tr>
                @endforelse
            </tbody>
        </table>

        <div class="mt-4">
            {{ $products->links() }}
        </div>
    </div>
</div>

モーダルコンポーネント

// app/Livewire/ProductModal.php
class ProductModal extends Component
{
    public bool $show = false;
    public ?Product $product = null;

    // 他のコンポーネントからのイベントをリッスン
    #[On('open-product-modal')]
    public function openModal(int $productId): void
    {
        $this->product = Product::find($productId);
        $this->show    = true;
    }

    public function closeModal(): void
    {
        $this->show    = false;
        $this->product = null;
    }

    public function render()
    {
        return view('livewire.product-modal');
    }
}
{{-- モーダルのBladeテンプレート --}}
<div>
    @if($show)
        <div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center"
             wire:click.self="closeModal">
            <div class="bg-white rounded-xl p-6 max-w-lg w-full mx-4 shadow-2xl">
                <div class="flex justify-between items-start mb-4">
                    <h2 class="text-xl font-bold">{{ $product->name }}</h2>
                    <button wire:click="closeModal" class="text-gray-400 hover:text-gray-600">✕</button>
                </div>
                <p class="text-2xl font-bold text-blue-600">¥{{ number_format($product->price) }}</p>
                <p class="text-gray-600 mt-2">{{ $product->description }}</p>
            </div>
        </div>
    @endif
</div>

パフォーマンス最適化

// Lazy Loading(表示領域に入ったときに読み込む)
class HeavyDashboard extends Component
{
    use WithoutUrlPagination;

    public function placeholder(): string
    {
        return <<<HTML
            <div class="animate-pulse bg-gray-200 h-64 rounded-xl"></div>
        HTML;
    }

    public function render()
    {
        return view('livewire.heavy-dashboard');
    }
}
{{-- Lazy Loading --}}
<livewire:heavy-dashboard lazy />

まとめ

Laravel Livewireは、JavaScriptの学習コストなしにリアルタイムUIを実装できる強力なツールです。検索フィルタ・ページネーション・モーダル・フォームバリデーションなど、よくある動的UI要件のほとんどをPHPだけで対応できます。弊社では予約管理システムの管理画面でLivewireを採用し、開発工数を大幅に削減しました。

LaravelによるWebシステム開発のご相談はお気軽にどうぞ。

Category 技術ブログ

Related Posts

関連記事

開発・技術のご相談はお気軽に

お見積りは無料です。まずはお気軽にご相談ください。

お問い合わせ →