Livewire in WordPress via Acorn
When to Use Livewire
| Approach |
Best for |
| Livewire |
Interactive server-driven UI, forms with validation, real-time filtering, pagination, file uploads |
| Blade Component |
Static display, no interactivity |
| Alpine.js |
Client-only UI (dropdowns, modals, tabs) — no server needed |
| REST API + JS |
High-frequency updates (> 1/sec), large datasets, offline support |
| ACF Block |
Editor-managed Gutenberg content |
Prerequisites
- Sage + Acorn installed
roots/acorn-livewire and livewire/livewire installed
lando theme-composer require roots/acorn-livewire livewire/livewire
lando acorn vendor:publish --tag=livewire:config
Add to resources/views/layouts/app.blade.php:
<head>
@livewireStyles
...
</head>
<body>
...
@livewireScripts
</body>
Quick Start
# Check installed versions
bash skills/acorn-livewire/scripts/check-versions.sh
# Create a component (validates PascalCase, checks lando availability)
bash skills/acorn-livewire/scripts/create-component.sh ContactForm
# → app/Livewire/ContactForm.php
# → resources/views/livewire/contact-form.blade.php
Use the component in any Blade template:
<livewire:contact-form />
<livewire:contact-form :post-type="$postType" />
Component Anatomy
class PostSearch extends Component
{
public string $query = '';
public function mount(string $postType = 'post'): void
{
// wp_set_current_user(get_current_user_id()); // if WP user context needed
$this->postType = $postType;
}
#[Computed]
public function posts(): array
{
return get_posts(['s' => $this->query, 'posts_per_page' => 10]);
}
public function render(): \Illuminate\View\View
{
return view('livewire.post-search');
}
}
{{-- resources/views/livewire/post-search.blade.php --}}
<div> {{-- single root element required --}}
<input wire:model.live.debounce.300ms="query" placeholder="Search..." />
@forelse ($this->posts as $post)
<a href="{{ get_permalink($post) }}">{{ $post->post_title }}</a>
@empty
<p>No results.</p>
@endforelse
</div>
wire:model Variants
| Directive |
When to use |
wire:model="name" |
Default — syncs on form submit (no extra requests) |
wire:model.live="name" |
Real-time — sends request on every keystroke |
wire:model.blur="name" |
Field-level validation on focus loss |
wire:model.live.debounce.300ms="name" |
Real-time search (debounced) |
Scripts
# Create a Livewire component (PascalCase name required)
bash skills/acorn-livewire/scripts/create-component.sh <ComponentName>
# Check package versions (Livewire, Acorn, PHP)
bash skills/acorn-livewire/scripts/check-versions.sh
Scripts: scripts/create-component.sh · scripts/check-versions.sh
Assets
Boilerplate templates with {{PLACEHOLDER}} tokens. Copy and replace placeholders.
- component.php.tpl — Standard component class:
extends Component, WithPagination commented out, mount() stub, wp_set_current_user stub, render().
- view.blade.php.tpl — Livewire Blade view: single
<div> root, wire:loading spinner, wire:model input example, Tailwind v4 note.
References
Deep content loaded on demand — zero tokens until needed.
- sage-integration.md — Acorn boot,
@livewireStyles/@livewireScripts, layout setup, Blade tag syntax, View Composer data passing, sage-html-forms comparison.
- state-patterns.md —
wire:model variants, #[Computed] properties, lifecycle hooks (mount, hydrate, updated), loading states, Form objects (v3), component events.
- alpine-interop.md —
$wire.entangle, combining Alpine + Livewire, $wire.propertyName, Tailwind v4 coordination, dynamic class names, skeleton loaders.
- file-uploads.md —
WithFileUploads trait, media_handle_sideload(), Lando storage paths, S3 driver, temporary file cleanup, validation rules.
- common-errors.md — "Unable to find component", hydration errors, 419 CSRF, Alpine conflicts, missing root
<div>, 404 on Livewire endpoint, wire:model not syncing.
Verification
- Render the component in a Blade template and confirm it appears with the correct initial state.
- Interact with
wire:click, wire:submit, or wire:model bindings and confirm the component updates reactively without a full page reload.
- Open browser DevTools Network tab and verify Livewire AJAX requests return 200 with the expected payload.
Failure modes
Problem: Component not found / blank render
- Cause: Blade tag does not match the class name, or the class is in the wrong namespace.
- Fix: Verify the class is in
app/Livewire/ with namespace App\Livewire. Tag convention: ContactForm → <livewire:contact-form />. Always generate with lando acorn make:livewire.
Problem: Hydration errors
- Cause: A public property holds a non-serializable value (
WP_Post, closures, resource handles).
- Fix: Store only scalar values. Use
#[Computed] for derived data. See references/common-errors.md.
For all other failure modes (419 CSRF, Alpine conflicts, 404 on Livewire endpoint) see references/common-errors.md.
Critical Rules
- Always wrap component views in a single root
<div>. Livewire requires exactly one root element per component view — multiple roots or naked text break hydration.
- Use
lando acorn make:livewire — never create component files manually. The generator sets correct namespaces, file paths, and naming conventions.
wire:model.live for real-time; wire:model for form submit. Each .live event is an HTTP request. Default to wire:model and use .live only when the UX genuinely requires instant server feedback.
wp_set_current_user for user context. If the component calls current_user_can() or any WP function that relies on the current user, call wp_set_current_user(get_current_user_id()) in mount().
- Never store large collections in public properties. Livewire serializes all public properties on every request. Use
#[Computed] for post lists and other derived data.
- Do not load Alpine separately. Livewire bundles Alpine. A second Alpine instance causes conflicts. See
references/alpine-interop.md.
@livewireStyles in <head>, @livewireScripts before </body>. Both are required. Missing either causes Livewire to silently fail.
Query First — MCP Integration
Before creating or referencing a Livewire component, query the live environment:
discover-abilities
execute-ability livewire/components
Use real class names from the query. Do not invent component names.
If the stack is not installed, run /ai-setup first.
See sageing/references/mcp-query-patterns.md for the full pattern.
1---2name: superpowers-sage-acorn-livewire3description: Livewire v3 in WordPress via Acorn: Livewire component, wire:model, wire:model.live, @livewire directive, reactive component, livewire component, make:livewire, Alpine + Livewire, wire:click, wire:submit, WithFileUploads, computed properties, mount lifecycle, hydrate, Acorn Livewire, roots/acorn-livewire, real-time UI, reactive forms — interactive server-driven UI without writing JavaScript in Sage themes4---56# Livewire in WordPress via Acorn78## When to Use Livewire910| Approach | Best for |11|---|---|12| **Livewire** | Interactive server-driven UI, forms with validation, real-time filtering, pagination, file uploads |13| **Blade Component** | Static display, no interactivity |14| **Alpine.js** | Client-only UI (dropdowns, modals, tabs) — no server needed |15| **REST API + JS** | High-frequency updates (> 1/sec), large datasets, offline support |16| **ACF Block** | Editor-managed Gutenberg content |1718## Prerequisites1920- Sage + Acorn installed21- `roots/acorn-livewire` and `livewire/livewire` installed2223```bash24lando theme-composer require roots/acorn-livewire livewire/livewire25lando acorn vendor:publish --tag=livewire:config26```2728Add to `resources/views/layouts/app.blade.php`:2930```blade31<head>32 @livewireStyles33 ...34</head>35<body>36 ...37 @livewireScripts38</body>39```4041## Quick Start4243```bash44# Check installed versions45bash skills/acorn-livewire/scripts/check-versions.sh4647# Create a component (validates PascalCase, checks lando availability)48bash skills/acorn-livewire/scripts/create-component.sh ContactForm49# → app/Livewire/ContactForm.php50# → resources/views/livewire/contact-form.blade.php51```5253Use the component in any Blade template:5455```blade56<livewire:contact-form />57<livewire:contact-form :post-type="$postType" />58```5960## Component Anatomy6162```php63class PostSearch extends Component64{65 public string $query = '';6667 public function mount(string $postType = 'post'): void68 {69 // wp_set_current_user(get_current_user_id()); // if WP user context needed70 $this->postType = $postType;71 }7273 #[Computed]74 public function posts(): array75 {76 return get_posts(['s' => $this->query, 'posts_per_page' => 10]);77 }7879 public function render(): \Illuminate\View\View80 {81 return view('livewire.post-search');82 }83}84```8586```blade87{{-- resources/views/livewire/post-search.blade.php --}}88<div> {{-- single root element required --}}89 <input wire:model.live.debounce.300ms="query" placeholder="Search..." />9091 @forelse ($this->posts as $post)92 <a href="{{ get_permalink($post) }}">{{ $post->post_title }}</a>93 @empty94 <p>No results.</p>95 @endforelse96</div>97```9899## wire:model Variants100101| Directive | When to use |102|---|---|103| `wire:model="name"` | Default — syncs on form submit (no extra requests) |104| `wire:model.live="name"` | Real-time — sends request on every keystroke |105| `wire:model.blur="name"` | Field-level validation on focus loss |106| `wire:model.live.debounce.300ms="name"` | Real-time search (debounced) |107108## Scripts109110```bash111# Create a Livewire component (PascalCase name required)112bash skills/acorn-livewire/scripts/create-component.sh <ComponentName>113114# Check package versions (Livewire, Acorn, PHP)115bash skills/acorn-livewire/scripts/check-versions.sh116```117118Scripts: [`scripts/create-component.sh`](scripts/create-component.sh) · [`scripts/check-versions.sh`](scripts/check-versions.sh)119120## Assets121122Boilerplate templates with `{{PLACEHOLDER}}` tokens. Copy and replace placeholders.123124- **[component.php.tpl](assets/component.php.tpl)** — Standard component class: `extends Component`, `WithPagination` commented out, `mount()` stub, `wp_set_current_user` stub, `render()`.125- **[view.blade.php.tpl](assets/view.blade.php.tpl)** — Livewire Blade view: single `<div>` root, `wire:loading` spinner, `wire:model` input example, Tailwind v4 note.126127## References128129Deep content loaded on demand — zero tokens until needed.130131- **[sage-integration.md](references/sage-integration.md)** — Acorn boot, `@livewireStyles`/`@livewireScripts`, layout setup, Blade tag syntax, View Composer data passing, `sage-html-forms` comparison.132- **[state-patterns.md](references/state-patterns.md)** — `wire:model` variants, `#[Computed]` properties, lifecycle hooks (`mount`, `hydrate`, `updated`), loading states, Form objects (v3), component events.133- **[alpine-interop.md](references/alpine-interop.md)** — `$wire.entangle`, combining Alpine + Livewire, `$wire.propertyName`, Tailwind v4 coordination, dynamic class names, skeleton loaders.134- **[file-uploads.md](references/file-uploads.md)** — `WithFileUploads` trait, `media_handle_sideload()`, Lando storage paths, S3 driver, temporary file cleanup, validation rules.135- **[common-errors.md](references/common-errors.md)** — "Unable to find component", hydration errors, 419 CSRF, Alpine conflicts, missing root `<div>`, 404 on Livewire endpoint, `wire:model` not syncing.136137## Verification138139- Render the component in a Blade template and confirm it appears with the correct initial state.140- Interact with `wire:click`, `wire:submit`, or `wire:model` bindings and confirm the component updates reactively without a full page reload.141- Open browser DevTools Network tab and verify Livewire AJAX requests return 200 with the expected payload.142143## Failure modes144145### Problem: Component not found / blank render146147- **Cause:** Blade tag does not match the class name, or the class is in the wrong namespace.148- **Fix:** Verify the class is in `app/Livewire/` with namespace `App\Livewire`. Tag convention: `ContactForm` → `<livewire:contact-form />`. Always generate with `lando acorn make:livewire`.149150### Problem: Hydration errors151152- **Cause:** A public property holds a non-serializable value (`WP_Post`, closures, resource handles).153- **Fix:** Store only scalar values. Use `#[Computed]` for derived data. See `references/common-errors.md`.154155For all other failure modes (419 CSRF, Alpine conflicts, 404 on Livewire endpoint) see [`references/common-errors.md`](references/common-errors.md).156157## Critical Rules1581591. **Always wrap component views in a single root `<div>`.** Livewire requires exactly one root element per component view — multiple roots or naked text break hydration.1602. **Use `lando acorn make:livewire` — never create component files manually.** The generator sets correct namespaces, file paths, and naming conventions.1613. **`wire:model.live` for real-time; `wire:model` for form submit.** Each `.live` event is an HTTP request. Default to `wire:model` and use `.live` only when the UX genuinely requires instant server feedback.1624. **`wp_set_current_user` for user context.** If the component calls `current_user_can()` or any WP function that relies on the current user, call `wp_set_current_user(get_current_user_id())` in `mount()`.1635. **Never store large collections in public properties.** Livewire serializes all public properties on every request. Use `#[Computed]` for post lists and other derived data.1646. **Do not load Alpine separately.** Livewire bundles Alpine. A second Alpine instance causes conflicts. See `references/alpine-interop.md`.1657. **`@livewireStyles` in `<head>`, `@livewireScripts` before `</body>`.** Both are required. Missing either causes Livewire to silently fail.166167## Query First — MCP Integration168169Before creating or referencing a Livewire component, query the live environment:170171```172discover-abilities173execute-ability livewire/components174```175176Use real class names from the query. Do not invent component names.177If the stack is not installed, run `/ai-setup` first.178See [`sageing/references/mcp-query-patterns.md`](../sageing/references/mcp-query-patterns.md) for the full pattern.