公開日 2026-04-03

LaravelでLivewireを始める

Laravel 13 の fresh app に Livewire を追加し、Blade へ component を差し込んで wire:model、wire:click、wire:submit を確認する。

目次

  1. 前提環境
  2. 1. ゴールと非対象
  3. 2. 先に全体像をつかむ
  4. 3. Livewire と Book model を追加する
  5. 4. Livewire component を作る
  6. 5. Blade に差し込む
  7. 6. サンプルデータを tinker で投入する
  8. 7. 画面で確認する
  9. 8. 詰まりやすい点
  10. 画面は出るが入力しても反応しない
  11. Component [book-manager] not found が出る
  12. 検索が Enter キーを押すまで反映されない
  13. 8000 番ポートが使えない
  14. 既定の single-file component で進めたい
  15. 9. まとめ

Blade の画面を保ったまま動的な UI を足したい人向けに、Laravel 13 の fresh app へ Livewire を入れる最小手順をまとめます。空ディレクトリから始める構成なので、既存の CRUD 記事や認証記事を先に実装していなくても進められます。既存シリーズは前提外です。

Livewire は、Blade を保ったままサーバー側の状態変更で UI を更新できる Laravel 向けの仕組みです。この記事では JavaScript フレームワークを別に組まず、一覧と追加フォームが動く最小形だけを扱います。

補助導線として先に読んでおくと入りやすい記事:

この 2 本を未読でも、以下の手順だけで完走できます。

前提環境

  • Windows 11
  • WSL2(Ubuntu)
  • VS Code(Remote - WSL)
  • Docker Desktop
  • composer:2 コンテナ
  • Laravel 13
  • Livewire 4
  • SQLite

以降のコマンドは、特記がない限り WSL 側ターミナルで実行します。

1. ゴールと非対象

この記事で到達する状態は次のとおりです。

  • http://localhost:8000/books に Livewire 付きの一覧画面を表示できる
  • wire:model.live で検索入力に合わせて一覧を絞り込める
  • wire:click で検索条件をクリアできる
  • wire:submit で本を追加し、そのまま一覧へ反映できる
  • Blade へ <livewire:book-manager /> を差し込む位置が分かる

今回は Livewire の入口に集中します。次の内容は扱いません。

  • Breeze / Starter Kit による認証導入
  • Volt や page component の使い分け
  • ページネーション、query string、複数条件検索
  • React / Vue / Inertia との比較
  • テスト、CI、デプロイ

2. 先に全体像をつかむ

流れは 5 段です。

  1. fresh app を作って Laravel を起動する
  2. Livewire を追加する
  3. Book model と一覧用 component を作る
  4. Blade に Livewire component を埋め込む
  5. tinker でデータを入れて /books を確認する

作業ディレクトリは ~/projects/laravel-livewire-intro-demo にそろえます。

mkdir -p ~/projects/laravel-livewire-intro-demo
cd ~/projects/laravel-livewire-intro-demo
code .

Laravel プロジェクトは composer:2 コンテナから作成します。ホスト側に PHP や Composer を直接入れずに済み、既存シリーズの手順にも合わせやすいからです。

docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app composer:2 create-project laravel/laravel .

Laravel 13 の fresh app では、この時点で .env 作成、APP_KEY 生成、database/database.sqlite の作成、初回 migration まで自動で進みます。

次に compose.yml を作成します。

services:
  app:
    image: composer:2
    user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
    working_dir: /app
    environment:
      HOME: /tmp
      XDG_CONFIG_HOME: /tmp/.config
    volumes:
      - ./:/app
    ports:
      - "8000:8000"
    command: php artisan serve --host=0.0.0.0 --port=8000

.env の該当部分は次のとおりです。

APP_URL=http://localhost:8000
DB_CONNECTION=sqlite
DB_DATABASE=/app/database/database.sqlite

続いてコンテナを起動します。

export LOCAL_UID="$(id -u)"
export LOCAL_GID="$(id -g)"
docker compose up -d

ブラウザで http://localhost:8000 を開き、Laravel の welcome 画面が見えれば準備完了です。

Bind for 0.0.0.0:8000 failed: port is already allocated が出た場合は、compose.yml の左側だけを 8001:8000 に変更し、.envAPP_URLhttp://localhost:8001 にそろえてください。

3. Livewire と Book model を追加する

まず Livewire を追加します。

docker compose exec app composer require livewire/livewire

Laravel 13 に合う Livewire 4 系が入り、実検証では v4.2.1 でした。

次に、一覧表示と追加フォームで使う Book model と migration を作成します。

docker compose exec app php artisan make:model Book -m

生成された database/migrations/*_create_books_table.php は次の内容に更新してください。timestamp 部分は実行時刻によって変わります。

<?php

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

return new class extends Migration
{
	public function up(): void
	{
		Schema::create('books', function (Blueprint $table) {
			$table->id();
			$table->string('title');
			$table->string('author');
			$table->unsignedInteger('price');
			$table->timestamps();
		});
	}

	public function down(): void
	{
		Schema::dropIfExists('books');
	}
};

app/Models/Book.php を次の内容に更新します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
	protected $fillable = [
		'title',
		'author',
		'price',
	];
}

ここまでできたら migration を実行します。

docker compose exec app php artisan migrate

4. Livewire component を作る

ここで BookManager component を作ります。

docker compose exec app php artisan make:livewire BookManager --class

Livewire 4 の既定は single-file component です。php artisan make:livewire BookManager だけを実行すると resources/views/components/⚡book-manager.blade.php が生成されます。今回は PHP クラスと Blade の接続を追いやすくするため、--class を付けて class-based component にします。

app/Livewire/BookManager.php を次の内容に更新してください。

<?php

namespace App\Livewire;

use App\Models\Book;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Validate;
use Livewire\Component;

class BookManager extends Component
{
	public string $search = '';

	#[Validate('required|string|max:255')]
	public string $title = '';

	#[Validate('required|string|max:255')]
	public string $author = '';

	#[Validate('required|integer|min:1')]
	public string $price = '';

	public function addBook(): void
	{
		$validated = $this->validate();

		Book::query()->create([
			'title' => trim($validated['title']),
			'author' => trim($validated['author']),
			'price' => (int) $validated['price'],
		]);

		$this->reset('title', 'author', 'price');
		session()->flash('status', '本を追加しました。');
	}

	public function clearSearch(): void
	{
		$this->reset('search');
	}

	public function render(): View
	{
		$books = Book::query()
			->when($this->search !== '', function ($query) {
				$query->where('title', 'like', '%' . $this->search . '%');
			})
			->orderBy('id')
			->get();

		return view('livewire.book-manager', [
			'books' => $books,
		]);
	}
}

このクラスで押さえたい点は 3 つです。

  • search は検索欄と結びつく public property です
  • clearSearch()wire:click から呼び出すメソッドです
  • addBook()wire:submit で呼び出し、バリデーション後に Book を保存します

pricestring にしているのは、input type="number" でもブラウザから Livewire へ届く値は最初は文字列だからです。保存時に (int) へ変換しておくと、入力時の扱いと DB へ入れる型の責務を分けやすくなります。

5. Blade に差し込む

次に component 用 Blade を作成します。resources/views/livewire/book-manager.blade.php は次の内容です。

<div class="book-manager">
	<div class="panel">
		<h2>Livewire で検索する</h2>
		<p>wire:model.live で title を絞り込みます。</p>

		<div class="inline-controls">
			<input wire:model.live="search" type="text" placeholder="タイトルで検索">
			<button wire:click="clearSearch" type="button">検索をクリア</button>
		</div>

		<p class="hint">現在の検索語: {{ $search === '' ? '未入力' : $search }}</p>

		<ul class="book-list">
			@forelse ($books as $book)
				<li wire:key="book-{{ $book->id }}">
					<strong>{{ $book->title }}</strong>
					<span>{{ $book->author }}</span>
					<span>{{ number_format($book->price) }}円</span>
				</li>
			@empty
				<li>一致する本はありません。</li>
			@endforelse
		</ul>
	</div>

	<div class="panel">
		<h2>wire:submit で本を追加する</h2>

		@if (session('status'))
			<p class="status">{{ session('status') }}</p>
		@endif

		<form wire:submit="addBook" class="book-form">
			<label>
				タイトル
				<input wire:model="title" type="text">
			</label>
			@error('title') <p class="error">{{ $message }}</p> @enderror

			<label>
				著者
				<input wire:model="author" type="text">
			</label>
			@error('author') <p class="error">{{ $message }}</p> @enderror

			<label>
				価格
				<input wire:model="price" type="number" min="1">
			</label>
			@error('price') <p class="error">{{ $message }}</p> @enderror

			<button type="submit">本を追加する</button>
		</form>
	</div>
</div>

wire:model.live を使っているのは、検索欄を入力するたびに一覧を更新したいからです。Livewire 4 では、修飾子なしの wire:model が毎キー入力で同期される前提ではありません。検索のような場面では .live を付けるほうが意図を伝えやすくなります。

Livewire component の public property は、対応する Blade から自動で参照できる仕組みです。そのため render() で渡しているのは $books だけですが、book-manager.blade.php 側では $search もそのまま使えます。

続いて、Livewire を埋め込むページ Blade を作成します。resources/views/books/index.blade.php は次の内容です。

<!doctype html>
<html lang="ja">
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>Laravel Livewire Intro</title>
		<style>
			body {
				margin: 0;
				font-family: sans-serif;
				background: #f5f7fb;
				color: #1f2937;
			}
			main {
				max-width: 960px;
				margin: 0 auto;
				padding: 40px 16px 64px;
			}
			h1 {
				margin-bottom: 8px;
			}
			.lead {
				margin-top: 0;
				color: #4b5563;
			}
			.book-manager {
				display: grid;
				gap: 24px;
			}
			.panel {
				background: #ffffff;
				border: 1px solid #dbe3f0;
				border-radius: 16px;
				padding: 24px;
				box-shadow: 0 12px 32px rgba(15, 23, 42, 0.08);
			}
			.inline-controls,
			.book-form {
				display: grid;
				gap: 12px;
			}
			input,
			button {
				font: inherit;
				padding: 10px 12px;
				border-radius: 10px;
				border: 1px solid #cbd5e1;
			}
			button {
				background: #2563eb;
				border-color: #2563eb;
				color: #ffffff;
				cursor: pointer;
			}
			.book-list {
				display: grid;
				gap: 12px;
				padding-left: 20px;
			}
			.book-list li {
				display: grid;
				gap: 4px;
			}
			.hint {
				color: #475569;
				font-size: 0.95rem;
			}
			.status {
				color: #166534;
				background: #dcfce7;
				border-radius: 10px;
				padding: 10px 12px;
			}
			.error {
				color: #b91c1c;
				margin: 0;
				font-size: 0.95rem;
			}
		</style>
		@livewireStyles
	</head>
	<body>
		<main>
			<h1>Laravel で Livewire を始めるデモ</h1>
			<p class="lead">Blade に Livewire コンポーネントを差し込み、入力・クリック・送信の 3 つを確認します。</p>

			<livewire:book-manager />
		</main>

		@livewireScripts
	</body>
</html>

@livewireStyles@livewireScripts は省略しないでください。ここが欠けると <livewire:book-manager /> 自体は HTML に出ても、ブラウザ上のやり取りは動きません。

最後にルートを作成します。routes/web.php を次の内容に更新してください。

<?php

use Illuminate\Support\Facades\Route;

Route::redirect('/', '/books');
Route::view('/books', 'books.index')->name('books.index');

ルート一覧を確認します。

docker compose exec app php artisan route:list --name=books
books.index を確認したルート一覧

books.index が見えていれば、Blade と Livewire をつなぐルートは用意できます。

6. サンプルデータを tinker で投入する

一覧画面を確認しやすいよう、先に 5 件の本を入れます。

docker compose exec app php artisan tinker --execute="App\\Models\\Book::query()->delete(); collect([[\"title\" => \"Laravel 入門\", \"author\" => \"山田 太郎\", \"price\" => 2800], [\"title\" => \"Laravel 実践 Livewire\", \"author\" => \"佐藤 花子\", \"price\" => 3400], [\"title\" => \"PHP と Web 基礎\", \"author\" => \"鈴木 一郎\", \"price\" => 2200], [\"title\" => \"Laravel テスト入門\", \"author\" => \"田中 次郎\", \"price\" => 3200], [\"title\" => \"Livewire UI パターン\", \"author\" => \"高橋 美咲\", \"price\" => 3600]])->each(fn (array \$attributes) => App\\Models\\Book::query()->create(\$attributes));"

\$attributes とエスケープしているのは、WSL 上の Bash が $attributes を先に展開しないようにするためです。ここを素の $attributes にすると、シェル展開の影響で tinker 実行時に parse error になりやすくなります。

7. 画面で確認する

ブラウザで http://localhost:8000/books を開いて、次の 3 つを順に試してください。

  1. 検索欄へ Laravel と入力し、Laravel 入門Laravel 実践 LivewireLaravel テスト入門 に絞られることを確認する
  2. 検索をクリア を押し、一覧が 5 件に戻ることを確認する
  3. フォームに Livewire 実践メモ検証 太郎3900 を入力して送信し、成功メッセージと新しい本がそのまま一覧へ出ることを確認する

wire:submit で保存したあとに一覧へ戻るルートを書いていないのは、同じ画面の中で状態が更新される感覚をつかみやすくするためです。POST 後に別ページへ戻さなくても画面が更新されるところが、Blade だけのフォーム送信との最初の違いになります。

Livewire の一覧画面と追加フォーム

8. 詰まりやすい点

画面は出るが入力しても反応しない

resources/views/books/index.blade.php@livewireStyles または @livewireScripts が欠けていないかを確認してください。HTML だけ表示され、Livewire の更新リクエストが飛ばない状態になりやすい箇所です。

Component [book-manager] not found が出る

php artisan make:livewire BookManager --class を実行した場合、埋め込み側は <livewire:book-manager /> です。class 名の大文字小文字ではなく、Blade 側は kebab-case で書きます。

検索が Enter キーを押すまで反映されない

検索欄を wire:model="search" にしていると、期待したタイミングで一覧が変わらないことがあります。今回のように入力のたびに絞り込みたい場合は wire:model.live="search" を使います。

8000 番ポートが使えない

compose.yml の左側だけを 8001:8000 のように変更し、.envAPP_URL も同じポートへそろえます。右側のコンテナポート 8000 はそのままで構いません。

既定の single-file component で進めたい

php artisan make:livewire BookManager だけを実行すると single-file component が生成されます。Livewire 4 の既定に寄せたい場合はその形でも構いませんが、初回は PHP クラスと Blade を分けたほうが責務を追いやすいので、本記事では --class を採用しました。

9. まとめ

fresh app から Livewire を入れ、Blade に component を差し込んで wire:model.livewire:clickwire:submit を一通り確認しました。Blade の延長で動的 UI を足す入口としては、この形が最も小さく始めやすい構成です。

次に進むなら、検索・並び替え・ページネーションまで Livewire へ寄せた一覧画面を作ると価値が見えやすくなります。補助導線として次の 2 本を置いておきます。

シリーズ 11/16

このシリーズ

Laravelの基本を最初から通す

  1. 1. Laravel入門(Route / Controller / View / Model 最小構成)
  2. 2. Laravelで最小CRUDを作る(一覧 / 作成 / 編集 / 削除)
  3. 3. Laravelで品質ゲートを敷く(PHPUnit / Larastan / Pint / GitHub Actions)
  4. 4. Laravelで認証を足す(Breeze 最小導入)
  5. 5. Laravelで認可を入れる(Policyで自分の本だけ編集できるようにする)
  6. 6. LaravelでQueueを始める(database queue + worker 最小構成)
  7. 7. Laravelでスケジューラを動かす(Command + Scheduler 最小構成)
  8. 8. Laravelでファイルアップロードを扱う(Storage + validation)
  9. 9. Laravelでリレーションを扱う(User / Book / Category の基本)
  10. 10. Laravelで検索・並び替え・ページネーション付き一覧を作る
  11. 11. LaravelでLivewireを始める 現在の記事
  12. 12. LaravelでLivewire一覧画面を作る(検索・並び替え・ページネーション)
  13. 13. LaravelでSanctum認証APIを作る
  14. 14. LaravelでBlade / Livewire / Inertia をどう使い分けるか
  15. 15. LaravelでInertia + Vue.js を始める
  16. 16. LaravelでInertia + React を始める