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:

bash
composer require livewire/livewire

Output yang Dihasilkan:

text
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/livewire

Versi 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:

bash
php artisan make:livewire counter --class

Output yang Dihasilkan:

text
   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
<?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:

blade
{{-- 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">&minus;</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:

text
<div wire:key="(kunci acak)" wire:snapshot="(JSON state awal: {&quot;data&quot;:{&quot;jumlah&quot;: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"
    >&minus;</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:

php
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:

text
  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:

php
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(),
        ]);
    }
}
blade
{{-- 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:

php
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:

text
  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 #

SituasiPenilaian
Form kompleks multi-langkah dengan validasi serverSangat cocok — properti dan validasi Laravel menyatu
Tabel dengan filter, sort, paginationCocok — state di server, hemat JavaScript tulisan tangan
UI yang butuh respons < 16 ms per interaksiKurang cocok — tiap aksi adalah round-trip
Editor kolaboratif, canvas, drag-drop intensifTidak cocok — butuh state lokal seperti Vue/React
Tim tanpa keahlian JavaScript ekosistemJustru 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.