Alpine mengurus state yang hidup di browser. Tapi banyak fitur web justru berputar di sekitar data server: form yang menyimpan ke database, tabel yang tersaring, upload foto. Menulis ini dengan Alpine berarti menyiapkan endpoint, fetch, penanganan error, dan sinkronisasi state — satu per satu, untuk tiap fitur. Livewire mengambil alih pekerjaan itu: kita menulis component PHP biasa dengan properti publik, dan Livewire yang menyinkronkan tiap perubahan ke browser lewat AJAX lalu memperbarui DOM secara otomatis. Pada chapter ini kita memasang Livewire sungguhan di sandbox, membangun dua component, dan menguji perilakunya lewat test — semuanya output nyata.
Instalasi #
Livewire adalah package Composer, bukan package npm:
composer require livewire/livewireOutput yang Dihasilkan:
80 packages you are using are looking for funding.
Use "composer fund" command to find out more!
> @php artisan vendor:publish --tag=laravel-assets --ansi --force
INFO No publishable resources for tag [laravel-assets].
No security vulnerability advisories found.
Using version ^4.4 for livewire/livewireVersi 4.4 terpasang. Livewire menyuntikkan JavaScript runtime-nya secara otomatis ke halaman mana pun yang memuat component — pada pengujian kami, halaman dengan <livewire:counter /> mendapat tambahan <script src=".../livewire.js"> tanpa kita menulis @vite khusus untuknya.
Anatomi Component: Class PHP plus View Blade #
Generator bawaan Livewire 4 membuat single-file component (class PHP dan markup dalam satu berkas di resources/views/components/). Untuk belajar anatomi, kita pakai bentuk klasik dua berkas yang eksplisit:
php artisan make:livewire counter --classOutput yang Dihasilkan:
INFO Livewire component [app/Livewire/Counter.php] created successfully.Class-nya adalah PHP murni turunan Livewire\Component, dan properti public adalah state yang otomatis disinkronkan:
<?php
namespace App\Livewire;
use Livewire\Component;
class Counter extends Component
{
public int $jumlah = 0;
public function tambah(): void
{
$this->jumlah++;
}
public function kurang(): void
{
if ($this->jumlah > 0) {
$this->jumlah--;
}
}
public function render()
{
return view('livewire.counter');
}
}View-nya adalah Blade biasa dengan direktif wire:* — kerabat dekat direktif x-* Alpine:
{{-- resources/views/livewire/counter.blade.php --}}
<div class="flex items-center gap-3">
<button wire:click="kurang" class="h-9 w-9 rounded-lg bg-white text-lg text-slate-700 ring-1 ring-slate-300">−</button>
<span class="w-10 text-center text-lg font-bold tabular-nums">{{ $jumlah }}</span>
<button wire:click="tambah" class="h-9 w-9 rounded-lg bg-indigo-600 text-lg text-white">+</button>
</div>Satu hal penting: $jumlah adalah properti class PHP yang sama dengan yang ditampilkan {{ $jumlah }} di view. Ketika wire:click="tambah" terpicu, Livewire menjalankan tambah() di server, me-render ulang view, lalu mengirim potongan HTML yang berubah ke browser. Inilah render awal component tersebut di halaman kami:
<div wire:key="(kunci acak)" wire:snapshot="(JSON state awal: {"data":{"jumlah":0}, ... checksum})"
wire:effects="[]" wire:id="(id acak per render)" wire:name="counter" class="flex items-center gap-3">
<button
wire:click="kurang"
class="h-9 w-9 rounded-lg bg-white text-lg text-slate-700 ring-1 ring-slate-300"
>−</button>
<span class="w-10 text-center text-lg font-bold tabular-nums">0</span>
<button
wire:click="tambah"
class="h-9 w-9 rounded-lg bg-indigo-600 text-lg text-white"
>+</button>
</div>Livewire menandai root component dengan atribut wire:id, wire:name, dan wire:snapshot — snapshot berisi kondisi state awal (di sini jumlah: 0) beserta checksum untuk mencegah pemalsuan. Tidak ada satu baris JavaScript yang kita tulis untuk semua ini.
Counter Teruji Tanpa Browser #
Testing helper Livewire memungkinkan kita mensimulasikan interaksi langsung di feature test — menekan tombol berarti memanggil method, dan state divalidasi dengan assertSet:
use App\Livewire\Counter;
use Livewire\Livewire;
public function test_tombol_tambah_dan_kurang_mengubah_state(): void
{
Livewire::test(Counter::class)
->call('tambah')
->call('tambah')
->call('tambah')
->assertSet('jumlah', 3)
->call('kurang')
->assertSet('jumlah', 2)
->assertSee('>2</span>', false);
}Output yang Dihasilkan:
PASS Tests\Feature\CounterLivewireTest
✓ counter merender kondisi awal 0.19s
✓ tombol tambah dan kurang mengubah state 0.02s
✓ jumlah tidak turun di bawah nol 0.04s
Tests: 3 passed (8 assertions)Test ketiga (jumlah tidak turun di bawah nol) membuktikan guard di method kurang() bekerja. Test seperti ini berjalan dalam milidetik karena tidak membutuhkan browser sungguhan.
wire:model: Form yang Menyaring Sendiri #
Potensi Livewire paling terasa pada wire:model — binding dua arah untuk elemen form. Component pencarian tugas kami memfilter daftar setiap kali pengguna mengetik:
class DaftarTugas extends Component
{
public string $cari = '';
public array $tugas = [
['judul' => 'Belajar Vite', 'status' => 'Selesai'],
['judul' => 'Rakit UI kit', 'status' => 'Berjalan'],
['judul' => 'Deploy ke produksi', 'status' => 'Terlambat'],
];
public function tugasTersaring(): array
{
if ($this->cari === '') {
return $this->tugas;
}
return array_values(array_filter(
$this->tugas,
fn (array $t) => str_contains(strtolower($t['judul']), strtolower($this->cari)),
));
}
public function render()
{
return view('livewire.daftar-tugas', [
'hasil' => $this->tugasTersaring(),
]);
}
}{{-- resources/views/livewire/daftar-tugas.blade.php --}}
<div class="space-y-3">
<input type="text" wire:model.live="cari" placeholder="Cari tugas..."
class="w-full rounded-lg px-3 py-2 text-sm ring-1 ring-slate-300">
<ul class="space-y-1">
@forelse ($hasil as $tugas)
<li class="flex items-center justify-between rounded-lg px-3 py-2 text-sm ring-1 ring-slate-200">
<span>{{ $tugas['judul'] }}</span>
<span class="text-xs text-slate-500">{{ $tugas['status'] }}</span>
</li>
@empty
<li class="rounded-lg px-3 py-2 text-sm text-slate-500">Tidak ada tugas yang cocok.</li>
@endforelse
</ul>
</div>Modifier .live membuat tiap ketikan terkirim ke server begitu terjadi — ketika halaman memfilter data besar atau berbasis query database, inilah yang kita butuhkan. Reaktivitasnya kami uji end-to-end:
Livewire::test(DaftarTugas::class)
->assertSee('Belajar Vite')
->assertSee('Deploy ke produksi')
// ketik "vite" -> daftar tersaring
->set('cari', 'vite')
->assertSee('Belajar Vite')
->assertDontSee('Deploy ke produksi')
// ketik kata yang tidak ada -> pesan kosong
->set('cari', 'xyz')
->assertSee('Tidak ada tugas yang cocok.');Output yang Dihasilkan:
PASS Tests\Feature\DaftarTugasLivewireTest
✓ pencarian wire model menyaring daftar 0.19s
Tests: 1 passed (6 assertions)Perhatikan apa yang tidak kami lakukan: tidak menulis endpoint, tidak menulis fetch, tidak menjaga dua state tetap sinkron. Livewire menjalankan semua plumbing itu, dan yang tersisa di berkas kita adalah PHP dan Blade.
Kapan Livewire Bersinar, Kapan Tidak #
| Situasi | Penilaian |
|---|---|
| Form kompleks multi-langkah dengan validasi server | Sangat cocok — properti dan validasi Laravel menyatu |
| Tabel dengan filter, sort, pagination | Cocok — state di server, hemat JavaScript tulisan tangan |
| UI yang butuh respons < 16 ms per interaksi | Kurang cocok — tiap aksi adalah round-trip |
| Editor kolaboratif, canvas, drag-drop intensif | Tidak cocok — butuh state lokal seperti Vue/React |
| Tim tanpa keahlian JavaScript ekosistem | Justru titik kuatnya |
Ekosistem Livewire juga patut dikenal sejak awal: Filament — admin panel builder di atas Livewire — begitu dominan sehingga banyak aplikasi Laravel production hampir tidak menulis UI manual sama sekali. Dengan tiga opsi frontend kini di tangan (Blade murni, Alpine, Livewire), chapter penutup section ini menyusun kerangka keputusan untuk memilih di antara mereka, ditambah Inertia dan SPA terpisah yang belum kita bahas.