v1 LiVue v1 is here — server-driven reactivity for Laravel using Vue.js Get Started →

Components

LiVue components are PHP classes that define reactive state and actions, paired with Blade templates that use Vue directives for interactivity. Every component extends the abstract Component base class and implements a render() method that returns a Blade view name.

Basic Structure

A LiVue component consists of two parts: a PHP class that extends LiVue\Component and a Blade template. The class defines reactive state through public properties and callable actions through public methods. The render() method tells LiVue which Blade view to use as the template.

app/LiVue/Counter.php
<?php

namespace App\LiVue;

use LiVue\Component;

class Counter extends Component
{
    public int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }

    protected function render(): string
    {
        return 'livue.counter';
    }
}
resources/views/livue/counter.blade.php
<div>
    <p>Count: {{ count }}</p>

    <button v-click="increment">
        Add
    </button>
</div>

Public properties become reactive Vue state, automatically synchronized between the PHP server and the Vue client. Public methods become callable actions that can be triggered from templates using directives like v-click.

Tip — Generate a new component with php artisan make:livue Counter. This creates both the PHP class and the Blade template.

Properties

Every public property on a LiVue component becomes reactive Vue state. Properties are serialized into a JSON snapshot sent to the client. When the client updates a property (e.g., via v-model), the new value is sent back to the server on the next AJAX request.

app/LiVue/UserProfile.php
class UserProfile extends Component
{
    // String
    public string $name = '';

    // Integer
    public int $age = 0;

    // Float
    public float $rating = 0.0;

    // Boolean
    public bool $active = true;

    // Array
    public array $items = [];

    // Nullable
    public ?string $email = null;
}

Supported Types

Type Example Notes
string '' Text values
int 0 Integer numbers
float 0.0 Decimal numbers
bool true Boolean flags
array [] Arrays and associative arrays
?type null Any type prefixed with ? for nullable

Type Casting with $casts

When the client sends values back to the server (e.g., from form inputs), everything arrives as a string. LiVue resolves types using this priority: the $casts array first, then the PHP type hint via reflection, and finally no cast at all.

app/LiVue/Settings.php
class Settings extends Component
{
    public $count = 0;
    public $price = 0.0;
    public $enabled = false;
    public $tags = '[]';

    protected array $casts = [
        'count' => 'int',
        'price' => 'float',
        'enabled' => 'bool',
        'tags' => 'json',
    ];
}

Tip — When you use PHP type hints (e.g., public int $count), LiVue casts automatically via reflection. The $casts array is only needed for untyped properties or when you need json casting.

Actions

Public methods on your component become actions that can be called from templates. When an action is triggered (e.g., via v-click), LiVue sends an AJAX request to the server, executes the method, and returns the updated state and HTML.

app/LiVue/TodoList.php
class TodoList extends Component
{
    public array $items = [];
    public string $newItem = '';

    public function add(): void
    {
        $this->items[] = $this->newItem;
        $this->newItem = '';
    }

    public function remove(int $index): void
    {
        unset($this->items[$index]);
        $this->items = array_values(
            $this->items
        );
    }
}
resources/views/livue/todo-list.blade.php
<div>
    <input v-model="newItem"
        @keyup.enter="add()" />

    <button v-click="add">Add</button>

    <ul>
        <li v-for="(item, i) in items">
            {{ item }}
            <button v-click="['remove', i]">
                x
            </button>
        </li>
    </ul>
</div>

Method Protection

Not all methods are callable from the client. LiVue uses reflection to automatically protect sensitive methods. The following are blocked from client-side invocation:

Category Details
Component base methods Any method declared on LiVue\Component (render, getState, etc.)
Lifecycle hooks boot, mount, hydrate, dehydrate, updating, updated, rendering, rendered
Magic methods Any method prefixed with __
Non-public methods Protected and private methods

Note — Only public methods declared directly on your concrete component class are callable from the client. Methods inherited from the base Component class are automatically blocked.

Lifecycle Hooks

LiVue provides lifecycle hooks like boot(), mount(), hydrate(), updating(), and more. These are optional methods you define on your component class to run code at specific moments.

For a comprehensive guide covering instance hooks, property-specific hooks, trait-based hooks, lifecycle events, and observers, see the dedicated Lifecycle page.

Page Components

A page component is a LiVue component rendered as a full page inside a Blade layout. Use LiVue::route() to map a URL to a component class.

routes/web.php
use LiVue\Facades\LiVue;

LiVue::route('/dashboard', App\LiVue\Dashboard::class);

// With Laravel route chaining
LiVue::route('/users/{id}', App\LiVue\UserProfile::class)
    ->name('user.profile')
    ->middleware('auth');

Layout Resolution

LiVue resolves the layout in this priority order:

  1. #[Layout] attribute on the class
  2. $layout property on the component
  3. config('livue.layout') from config file
  4. Fallback: components.layouts.app

Title Resolution

The page title follows a similar priority:

  1. #[Title] attribute on the class
  2. $title property on the component
  3. config('app.name')
Via Attributes (recommended)
use LiVue\Attributes\Layout;
use LiVue\Attributes\Title;

#[Layout('layouts.dashboard')]
#[Title('User Profile')]
class UserProfile extends Component
{
    public string $name = '';

    public function mount(int $id): void
    {
        $user = User::findOrFail($id);
        $this->name = $user->name;
    }

    protected function render(): string
    {
        return 'livue.user-profile';
    }
}
Via Properties
class UserProfile extends Component
{
    protected ?string $layout =
        'layouts.dashboard';

    protected ?string $title =
        'User Profile';

    public string $name = '';

    public function mount(int $id): void
    {
        $user = User::findOrFail($id);
        $this->name = $user->name;
    }

    protected function render(): string
    {
        return 'livue.user-profile';
    }
}

Layout Template

Your layout must render the component HTML using unescaped output. Generate a layout with php artisan livue:layout.

resources/views/components/layouts/app.blade.php
<!DOCTYPE html>
<html>
<head>
    <title>{{ $title ?? config('app.name') }}</title>
    @livueHead
    @vite(['resources/js/app.js'])
</head>
<body>
    {!! $slot !!}
</body>
</html>

Note — Always use {!! $slot !!} (unescaped) because $slot contains rendered HTML from the component.

Head Management

Page components can inject dynamic elements into the <head> — meta tags, Open Graph, Twitter Cards, canonical URLs, and JSON-LD structured data. Use a $head property for static values or a head() method for dynamic values.

Static head (property)
class About extends Component
{
    protected array $head = [
        'description' => 'About our company',
        'og:title' => 'About Us',
        'og:image' => 'https://example.com/about.jpg',
        'twitter:card' => 'summary',
    ];
}
Dynamic head (method — takes priority)
class BlogPost extends Component
{
    public Post $post;

    protected function head(): array
    {
        return [
            'description' => $this->post->excerpt,
            'og:title' => $this->post->title,
            'og:description' => $this->post->excerpt,
            'og:image' => $this->post->featured_image,
            'twitter:card' => 'summary_large_image',
            'canonical' => route('blog.show', $this->post),
            'json-ld' => [
                '@context' => 'https://schema.org',
                '@type' => 'Article',
                'name' => $this->post->title,
            ],
        ];
    }
}

Supported Keys

Key Generated Tag
description, robots, keywords, ... <meta name="..." content="...">
og:*, article:* <meta property="..." content="...">
twitter:* <meta name="twitter:..." content="...">
canonical <link rel="canonical" href="...">
json-ld <script type="application/ld+json">...</script>

Note — Your layout must include the @livueHead directive in the <head> section (see the layout example above). All generated elements are marked with data-livue-head and are automatically updated during SPA navigation.

Priority

If both $head and head() are defined, the method takes priority over the property.

Nesting Components

LiVue components can be nested inside other components. Child components are rendered server-side and get their own independent Vue instance on the client. There are two equivalent syntaxes for including child components.

Inclusion Syntaxes

Directive syntax
<div>
    @livue('child-component')

    
    @livue('child-component', [
        'items' => $items,
        'title' => $title
    ])
</div>
Tag syntax
<div>
    <livue:child-component />

    
    <livue:child-component
        :items="$items"
        :title="$title" />
</div>

Passing Props via mount()

Child components receive props through their mount() method:

app/LiVue/ChildComponent.php
class ChildComponent extends Component
{
    public array $items = [];
    public string $title = '';

    public function mount(array $items = [], string $title = ''): void
    {
        $this->items = $items;
        $this->title = $title;
    }
}

Reactive Props with #[Reactive]

Mark a child property with #[Reactive] to automatically sync it when the parent re-renders:

Child with #[Reactive]
use LiVue\Attributes\Reactive;

class ItemList extends Component
{
    #[Reactive]
    public array $items = [];

    // $items auto-updates when
    // parent changes
}
Parent template
<div>
    <button v-click="addItem">
        Add Item
    </button>

    @livue('item-list', [
        'items' => $items
    ])
</div>

Communication Patterns

Child to Parent: dispatch()

Children communicate with parents by dispatching events:

Child dispatches event
class Modal extends Component
{
    public function close(): void
    {
        $this->dispatch('modal-closed');
    }
}
Parent listens
use LiVue\Attributes\On;

class Dashboard extends Component
{
    #[On('modal-closed')]
    public function onClosed(): void
    {
        // Handle modal closed
    }
}

Parent to Child: Refs

Parents can call methods or set properties on child components using refs:

Parent template with ref
<div>
    <button @click="livue.refs.myModal.call('open')">
        Open Modal
    </button>

    
    @livue('modal', [], ['ref' => 'myModal'])

    
    <livue:modal ref="myModal" />
</div>
Ref API Description
livue.refs.name.call(method, params) Call a method on the child
livue.refs.name.set(prop, value) Set a property on the child
livue.refs.name.dispatch(event, data) Dispatch an event from the child
livue.refs.name.sync() Sync child state with server
livue.refs.name.state Access child reactive state (read-only)

Single & Multi File Components

Beyond the standard class-based approach (separate PHP class + Blade template), LiVue supports two additional component styles: Single File Components (SFC) and Multi File Components (MFC).

Single File Component (SFC)

An SFC packs the PHP class and template into a single .blade.php file using an anonymous class. Ideal for simple, self-contained components.

resources/views/livue/counter.blade.php
<?php

use LiVue\Component;

new class extends Component {
    public int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }
};
?>

<div>
    <p>Count: {{ count }}</p>
    <button v-click="increment">+1</button>
</div>

SFC rules:

  • -- The file must start with <?php
  • -- The class must be anonymous: new class extends Component
  • -- The PHP block must end with ?>
  • -- The HTML template follows after the closing PHP tag
  • -- All LiVue features are supported (#[Computed], #[Validate], @script, etc.)

Tip — Generate an SFC with php artisan make:livue MyComponent --single

Multi File Component (MFC)

An MFC splits the component into separate files within a folder. The file names must match the folder name (kebab-case).

Folder structure
resources/views/livue/my-widget/
|-- my-widget.php          # Required: anonymous class
|-- my-widget.blade.php    # Required: template
|-- my-widget.js           # Optional: Vue Composition API
|-- my-widget.css          # Optional: scoped styles

The JS file content is automatically wrapped in a @script block. The CSS file is automatically scoped to the component using a data-livue-scope-{name} attribute selector prefixed to all rules.

Input CSS
.my-widget {
    padding: 1rem;
}

.my-widget button {
    color: blue;
}
Compiled (scoped)
[data-livue-scope-my-widget] .my-widget {
    padding: 1rem;
}

[data-livue-scope-my-widget] .my-widget button {
    color: blue;
}

Tip — Generate an MFC with php artisan make:livue MyComponent --multi

Resolution Order

When LiVue resolves a component by name, it searches in this order:

Priority Type Location
1 Registered LiVue::register()
2 Class-based App\LiVue\Name
3 Single File resources/views/livue/name.blade.php
4 Multi File resources/views/livue/name/
Aspect Class-based SFC MFC
Files 2 (PHP + Blade) 1 2-4
CSS Scoping No No Yes (automatic)
Separate JS No No (@script inline) Yes
Best for Complex components Simple components Components with dedicated styling

Islands

Islands are independent interactive regions that mount as separate Vue application instances, completely isolated from the parent component's Vue tree. Each island has its own state, reactivity, and AJAX updates.

When to Use Islands

  • -- Heavy components: Widgets with complex local state
  • -- Third-party integrations: Components that might conflict with parent reactivity
  • -- Performance isolation: Prevent large re-renders from affecting other parts of the page
  • -- State isolation: When you need completely independent state management

Declaring an Island

Via #[Island] attribute
use LiVue\Attributes\Island;

#[Island]
class AnalyticsWidget extends Component
{
    public array $data = [];

    public function refresh(): void
    {
        $this->data =
            AnalyticsService::getData();
    }
}
Via $island property
class AnalyticsWidget extends Component
{
    protected bool $island = true;

    public array $data = [];

    public function refresh(): void
    {
        $this->data =
            AnalyticsService::getData();
    }
}

How Islands Work

  • -- Separate Vue app: Each island creates its own Vue application instance
  • -- Independent state: Island state is completely isolated from the parent
  • -- Independent updates: AJAX updates to an island do not affect the parent
  • -- Own reactivity tree: Vue reactivity does not cross island boundaries

Combining with Lazy Loading

Islands work well with the #[Lazy] attribute for deferred, isolated components:

Island + Lazy
use LiVue\Attributes\Island;
use LiVue\Attributes\Lazy;

#[Island]
#[Lazy]
class HeavyWidget extends Component
{
    // Loads on intersection AND has its own Vue app
}

Tip — Nested islands are supported (islands within islands). Events dispatched from islands can still be caught by parent components using the global event bus.

Lazy Loading

Lazy-loaded components defer their rendering until they enter the viewport (using IntersectionObserver) or after the page loads. The server skips mount() during the initial render and only executes it when the component is actually needed.

The #[Lazy] Attribute

app/LiVue/LazyChart.php
use LiVue\Attributes\Lazy;

#[Lazy]
class LazyChart extends Component
{
    public array $data = [];

    public function placeholder(): string
    {
        return 'livue.chart-skeleton';
    }

    public function mount(): void
    {
        // Expensive query runs only when visible
        $this->data = DB::table('analytics')
            ->selectRaw('DATE(created_at) as date, COUNT(*) as count')
            ->groupBy('date')
            ->get()->toArray();
    }

    protected function render(): string
    {
        return 'livue.lazy-chart';
    }
}

Options

Usage Behavior
#[Lazy] Default: loads when the component enters the viewport
#[Lazy(onLoad: true)] Loads after the page finishes loading (requestAnimationFrame)

Placeholder

Define a placeholder() method to show a custom loading skeleton. It returns a Blade view name that will be displayed while the component loads. If not defined, LiVue uses its default placeholder.

resources/views/livue/chart-skeleton.blade.php
<div class="h-72 bg-gray-100 rounded-lg flex items-center justify-center">
    <span class="text-gray-400">Loading chart...</span>
</div>

How It Works

During the initial page render, the server does not mount the component. Instead, it outputs a <livue-lazy> wrapper with the placeholder content. On the client:

  1. The <livue-lazy> Vue component mounts
  2. IntersectionObserver monitors the element (or requestAnimationFrame if onLoad: true)
  3. When triggered, a pooled AJAX request is sent to /livue/update
  4. The server mounts the component, executes mount(), and returns HTML + snapshot
  5. The client creates the Vue component and replaces the placeholder

Request Pooling

If multiple lazy components enter the viewport in the same microtask, their load requests are batched into a single HTTP request. For example, 5 components becoming visible simultaneously result in just one AJAX call.

Tip — LiVue includes a MutationObserver that automatically detects lazy components added to the DOM dynamically (e.g., via Turbo or HTMX), so they work seamlessly in any rendering context.

Macros

The base Component class includes Laravel's Macroable trait, allowing you to register dynamic methods at runtime via closures. Macros are callable from both PHP and the client (AJAX).

Registering Macros

Register macros in a service provider's boot() method. Macros have access to $this (the component instance), just like regular methods.

app/Providers/AppServiceProvider.php
use LiVue\Component;

public function boot(): void
{
    Component::macro('greet', function () {
        return 'Hello, ' . $this->name;
    });

    Component::macro('setDefaults', function (string $name, int $count) {
        $this->name = $name;
        $this->count = $count;
    });
}

Calling from the Client

Registered macros are automatically callable from the frontend via v-click, direct calls, and all other action directives:

Blade template
<button v-click="greet">Greet</button>
<button v-click="['setDefaults', 'Test', 5]">Set Defaults</button>

Mixins

Group related macros into a class and register them all at once with mixin():

app/LiVue/Mixins/DateHelpers.php
class DateHelpers
{
    public function formatDate()
    {
        return function (string $field) {
            return $this->{$field}->format('d/m/Y');
        };
    }
}

Component::mixin(new DateHelpers());

Security

Macros follow the same security rules as regular methods:

Rule Description
Magic methods Names prefixed with __ are blocked
Lifecycle hooks boot, mount, hydrate, etc. are blocked
Trait utilities macro, hasMacro, flushMacros, mixin are not callable from client
Method precedence Real methods always take precedence over macros with the same name

Utility Methods

Component::hasMacro('greet');    // true
Component::flushMacros();          // Remove all macros