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

JavaScript

LiVue's client-side runtime, component API, global utilities, Vue integration and extension system.

Runtime Overview

LiVue includes a lightweight JavaScript runtime that turns server-rendered Blade templates into fully reactive Vue applications. Understanding how this runtime operates helps you write better components and debug issues more effectively.

The entire lifecycle follows a clear sequence:

  1. 1
    Server Renders Blade

    Laravel renders your Blade template on the server, producing complete HTML. The component's state and metadata are embedded as data-livue-snapshot attributes on the wrapper element.

  2. 2
    Browser Mounts Vue App

    On DOMContentLoaded, the runtime finds all [data-livue-id] elements, identifies root components, and creates a Vue application for each one.

  3. 3
    Dynamic Component Registration

    Nested child components are discovered in the HTML and registered as Vue components within the parent's app. Each child gets its own reactive state and livue helper instance.

  4. 4
    State Refs Created from Snapshot

    The JSON snapshot is parsed and wrapped in Vue's reactive(). A plain copy is kept as the server-confirmed baseline for computing diffs on the next request.

  5. 5
    AJAX Layer Handles Communication

    When a user triggers an action, the runtime computes the state diff, batches it with other pending calls from the same tick, and sends a single HTTP request. Multiple server calls in the same handler are automatically pooled into a single HTTP request.

  6. 6
    Template Swap on Response

    The server responds with the updated snapshot and re-rendered HTML. The runtime updates the reactive state, swaps the component definition via a shallowRef, and Vue re-renders the DOM. No DOM morphing — a clean template replacement.

Single App by Default — All components on a page share one Vue application. Root components and their children live in the same Vue instance. If you need isolation, mark a component as an Island to give it its own independent Vue app.

Component API (livue.*)

Every component template has access to a livue helper object. This object is available in your Blade templates and inside @script blocks. It provides methods for calling the server, managing state, watching changes, handling errors, and more.

Methods

Method Description
livue.toggle(prop) Toggle a boolean property on the client.
livue.set(prop, value) Set a property value on the client without a server call.
livue.store(name, definition, options?) Create a quick Pinia store, or reuse an existing one by calling it without definition.
livue.useStore(name) Resolve a pre-registered store (component scope first, then global).
livue.useGlobalStore(name) Resolve a pre-registered global store only.
livue.sync() Sync the current client state to the server without calling a method.
livue.watch(prop, callback) Watch a property for changes. Returns an unwatch function.
livue.upload(prop, file) Upload a file to the given property.
livue.dispatch(event, data) Dispatch an event to all listeners.
livue.dispatchTo(target, event, data) Dispatch an event to a specific component.
livue.navigate(url) Trigger SPA navigation to a URL.
livue.clearErrors() Clear all validation errors.

Properties

Property Type Description
livue.loading boolean True while any AJAX request is in flight.
livue.processing string|null Name of the method currently being processed.
livue.errors object Programmatic validation errors object (arrays per field). Template shorthand: $errors (returns first message string directly).
livue.$el HTMLElement The component's root DOM element.
livue.$id string The component's unique identifier.
livue.$name string The component name in kebab-case.
livue.$parent object|null Parent component's livue helper, or null for root.

Calling Server Methods

Server methods are available directly in the template — just call them like regular functions.

Blade template
<!-- Direct call (recommended) -->
<button @click="save()">Save</button>

<!-- With arguments -->
<button @click="delete(item.id)">Delete</button>

<!-- Multiple arguments -->
<button @click="update(item.id, { name: 'New Name' })">Update</button>

<!-- Declarative with v-click -->
<button v-click="save">Save</button>

Toggling Boolean Properties

livue.toggle() flips a boolean property on the client side, without making a server round-trip.

Blade template
<button @click="livue.toggle('showDetails')">
    {{ showDetails ? 'Hide' : 'Show' }} Details
</button>

<div v-if="showDetails">
    <p>Detailed content here...</p>
</div>

Watching State Changes

livue.watch() registers a callback that fires whenever a specific property changes. It returns an unwatch function you can call to stop listening.

@script block
// Watch a property for changes
const unwatch = livue.watch('count', (newValue, oldValue) => {
    console.log('Count changed from', oldValue, 'to', newValue);
});

// Stop watching when no longer needed
unwatch();

Quick Pinia Stores

If a store is declared in PHP, you can resolve it directly in @script with useStore(name) and sync frontend edits with livue.sync().

@script block
const componentStore = useStore('demo-counter');
const globalStore = livue.useGlobalStore('demo-global-counter');

// Frontend updates (local until sync)
componentStore.count++;
globalStore.count++;

const syncToBackend = () => livue.sync();

Stores declared from PHP expose state (and optional bridge rules). For frontend actions, mutate store state directly or call a PHP method, then use livue.sync() when you want to persist client changes.

Accessing the Root Element

livue.$el gives you a reference to the component's root DOM element. This is useful for interacting with third-party libraries or measuring layout.

@script block
// Access the root DOM element
const width = livue.$el.offsetWidth;
console.log('Component width:', width);

// Scroll into view
livue.$el.scrollIntoView({ behavior: 'smooth' });

Validation Errors

The $errors magic variable is the recommended way to display validation errors in templates. Each key returns the first error message string directly. The programmatic livue.errors object is also available (returns arrays per field).

Blade template
<div>
    <input v-model-livue="email" type="email">

    <!-- Show error for a specific field -->
    <p v-if="$errors.email" class="text-red-500">
        {{ $errors.email }}
    </p>

    <!-- Loop through all errors (programmatic API) -->
    <ul v-if="Object.keys(livue.errors).length">
        <li v-for="(messages, field) in livue.errors">
            {{ field }}: {{ messages[0] }}
        </li>
    </ul>

    <button @click="livue.clearErrors()">Clear Errors</button>
</div>

File Uploads

Use livue.upload() to send files to the server. You can track progress through the reactive livue.uploading and livue.uploadProgress properties.

Blade template
<input
    type="file"
    @change="livue.upload('avatar', $event.target.files[0])"
/>

<div v-if="livue.uploading">
    Uploading: {{ livue.uploadProgress }}%
</div>

Global API (LiVue.*)

The global LiVue object is available on window.LiVue. It provides methods for finding components, navigating, registering hooks, and handling errors at the application level.

Methods

Method Description
LiVue.find(id) Find a component instance by its unique ID.
LiVue.first() Get the first component on the page.
LiVue.all() Get all root and island component instances.
LiVue.getByName(name) Find all components matching a given name.
LiVue.hook(name, callback) Register a lifecycle hook callback. Returns an unsubscribe function.
LiVue.onError(callback) Register a global error handler.
LiVue.navigate(url) Trigger SPA navigation to a URL programmatically.
LiVue.setup(callback) Register a Vue app configuration callback.

Finding Components

Browser console or app.js
// Get the first component on the page
const component = LiVue.first();

// Get all root/island components
const components = LiVue.all();
console.log('Found', components.length, 'components');

// Find a specific component by ID
const cart = LiVue.find('livue-abc123');
console.log(cart.state);

// Find all components by name
const counters = LiVue.getByName('counter');
// Returns: [{ id, name, state, livue }, ...]

Global DOM Events

LiVue dispatches native DOM events on the document at key points in its lifecycle. These are useful for integrations that need to run before or after LiVue initializes, or after SPA navigations.

app.js
// Fired after LiVue loaded but before initialization
document.addEventListener('livue:init', () => {
    // Register hooks, plugins, etc.
});

// Fired after LiVue fully initialized
document.addEventListener('livue:initialized', () => {
    // LiVue is ready, all components mounted
});

// Fired after SPA navigation completes
document.addEventListener('livue:navigated', (event) => {
    console.log('Navigated to:', event.detail.url);
});

Vue Registration

LiVue.setup() is the entry point for configuring the Vue applications created by LiVue. It accepts a callback that receives the Vue app instance, allowing you to register plugins, components, directives, and anything else you would normally do with app.use() or app.component().

The callback runs for every Vue app LiVue creates — both root components and islands. You can call setup() multiple times; all callbacks are executed in registration order.

Basic Setup

Import LiVue from the ESM bundle and call setup() in your app.js. When setup() is called, LiVue automatically switches to ESM mode and shares your Vue instance instead of loading its standalone bundle.

resources/js/app.js
import LiVue from 'livue';

LiVue.setup((app) => {
    // app is a Vue app instance (from createApp())
    // Configure it however you need
});

Registering Global Components

resources/js/app.js
import LiVue from 'livue';
import MyButton from './components/MyButton.vue';
import DataTable from './components/DataTable.vue';

LiVue.setup((app) => {
    app.component('MyButton', MyButton);
    app.component('DataTable', DataTable);
});

Once registered, you can use these components directly in your Blade templates like any other Vue component: <MyButton label="Click me" />.

Registering Global Directives

resources/js/app.js
import LiVue from 'livue';

LiVue.setup((app) => {
    app.directive('focus', {
        mounted(el) {
            el.focus();
        }
    });

    app.directive('tooltip', {
        mounted(el, binding) {
            el.title = binding.value;
        },
        updated(el, binding) {
            el.title = binding.value;
        }
    });
});

Vite Alias

Since LiVue is distributed via Composer (not npm), you need a Vite alias to import it cleanly:

vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    resolve: {
        alias: {
            'livue': resolve(__dirname, 'vendor/livue/livue/dist/livue.esm.js'),
        },
    },
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});

Execution Order — When a Vue app is created, Pinia is installed first (required internally), then all setup() callbacks run in registration order, then LiVue's built-in directives are registered, and finally the app is mounted.

Extending LiVue

LiVue integrates naturally with the Vue ecosystem. Any Vue plugin, UI framework, or state management library that works with app.use() can be plugged in through LiVue.setup().

Vuetify

Install Vuetify with its theme and styles, then register it through the setup callback:

resources/js/app.js
import LiVue from 'livue';
import { createVuetify } from 'vuetify';
import 'vuetify/styles';

const vuetify = createVuetify({
    theme: {
        defaultTheme: 'dark'
    }
});

LiVue.setup((app) => {
    app.use(vuetify);
});

Pinia Stores

Pinia is already installed on every Vue app by LiVue (it uses Pinia internally). Define component stores in the component class and global stores at app bootstrap:

app/LiVue/StorePatternDemo.php
protected function defineStores(): array
{
    return [
        'demo-counter' => [
            'state' => ['count' => $this->serverCount],
            'bridge' => ['count' => 'serverCount'],
        ],
    ];
}
app/Providers/AppServiceProvider.php
use LiVue\Facades\LiVue;

public function boot(): void
{
    // Global stores are defined once at bootstrap
    LiVue::createStore('demo-global-counter', [
        'state' => ['count' => 100],
        'bridge' => ['count' => 'globalCount'],
    ]);

    LiVue::createStore('demo-app-counter', [
        'state' => ['count' => 500],
    ]);
}
@script block
const componentStore = useStore('demo-counter');
const globalStore = livue.useGlobalStore('demo-global-counter');
const appGlobalStore = livue.useGlobalStore('demo-app-counter');

componentStore.count++;
globalStore.count++;
appGlobalStore.count++;
resources/js/stores/cart.js (advanced)
import { defineStore } from 'pinia';

export const useCartStore = defineStore('cart', {
    state: () => ({ items: [] }),
});

Custom Vue Plugins

Register any Vue plugin through setup(). Multiple calls are cumulative, so frameworks built on LiVue can register their own plugins while still allowing users to add more:

resources/js/app.js
import LiVue from 'livue';
import { createVuetify } from 'vuetify';
import { createI18n } from 'vue-i18n';

const vuetify = createVuetify({ /* ... */ });
const i18n = createI18n({ /* ... */ });

// Register multiple plugins in one call
LiVue.setup((app) => {
    app.use(vuetify);
    app.use(i18n);
});

// Or spread across multiple setup() calls
LiVue.setup((app) => {
    app.use(vuetify);
});

LiVue.setup((app) => {
    app.use(i18n);
});

Plugin API

Configure plugins, components and directives inside LiVue.setup():

resources/js/app.js
LiVue.setup((app) => {
    app.use(MyPlugin, { option: 'value' });
    app.component('my-button', MyButton);
    app.directive('focus', focusDirective);
});

Note — In PHP, useStore() (or defineStores()) declares component stores, while LiVue::createStore() declares globals. Use useGlobalStore(name) only to resolve a global store.

Error Boundaries

LiVue provides built-in error handling for AJAX failures. By default, errors are logged to the browser console. You can customize this behavior at the global level or per component.

Default Behavior

When an AJAX request fails (network error, server exception, validation error), LiVue logs the error to the console and resets the component's loading state. The UI remains functional and the user can retry the action.

Global Error Handler

Register a custom error handler with LiVue.onError() to intercept all errors across your application. This is the ideal place to integrate toast notifications, error tracking services, or custom retry logic.

resources/js/app.js
import LiVue from 'livue';

// Simple error handler
LiVue.onError((error, componentName) => {
    console.error('Error on', componentName, ':', error);
});

Toast Notification Integration

A practical example showing how to display user-friendly toast messages when errors occur:

resources/js/app.js
import LiVue from 'livue';
import { toast } from 'vue-sonner';

LiVue.onError((error, componentName) => {
    // Show toast to the user
    toast.error('Something went wrong. Please try again.');

    // Send to error tracking service
    Sentry.captureException(error, {
        tags: { component: componentName }
    });
});

Component Error State

Components with the #[ErrorBoundary] PHP attribute expose a reactive livue.errorState object in the template. You can use it to display inline error messages and recovery options:

PHP Attribute
use LiVue\Attributes\ErrorBoundary;

#[ErrorBoundary(
    recover: true,
    showDetails: true,
    message: 'Oops! Something went wrong.'
)]
class Dashboard extends Component
{
    // ...
}
Blade template
<div v-if="livue.errorState.hasError"
     class="bg-red-900/20 border border-red-800 p-4 rounded">
    <p>{{ livue.errorState.errorMessage }}</p>

    <p v-if="livue.errorState.errorDetails"
       class="text-sm text-gray-400 mt-2">
        {{ livue.errorState.errorDetails }}
    </p>

    <button
        @click="livue.clearError()"
        v-if="livue.errorState.recover">
        Dismiss
    </button>
</div>

Error Hook

For more granular control, use the error.occurred lifecycle hook. It receives error details and a preventDefault() function to suppress default handling:

resources/js/app.js
LiVue.hook('error.occurred', ({ error, componentName, componentId, context, preventDefault }) => {
    // Prevent default error handling
    preventDefault();

    // Custom error reporting
    sendToErrorTracker({
        message: error.message,
        component: componentName,
        method: context.method,
    });
});

Client-side Hooks

LiVue.hook() lets you intercept lifecycle events across the entire application. Hooks are ideal for logging, analytics, custom loading indicators, and other cross-cutting concerns. Each call returns an unsubscribe function.

Available Hooks

Hook Description
component.init Fired when a component is initialized (root or child). Provides component, el, and a cleanup callback.
component.destroy Fired when a component is being destroyed.
element.init Fired when a component's DOM element is initialized.
request.started Fired when an AJAX request is sent. Provides url and updateCount.
request.finished Fired when an AJAX request completes. Provides url, success, and error.
template.updating Fired before a template is swapped. Provides component, el, and html.
template.updated Fired after a template swap completes. Provides component and el.
error.occurred Fired when an error occurs. Provides error, componentName, and preventDefault.

Component Lifecycle Tracking

resources/js/app.js
const unsubscribe = LiVue.hook('component.init', ({ component, el, cleanup }) => {
    console.log('Component initialized:', component.name);

    // Register a cleanup function that runs when the component is destroyed
    cleanup(() => {
        console.log('Component destroyed:', component.name);
    });
});

// Later, stop listening to all component.init events
unsubscribe();

Request Tracking

Use request hooks to build custom loading indicators, log performance metrics, or integrate with analytics:

resources/js/app.js
let requestStart;

LiVue.hook('request.started', ({ url, updateCount }) => {
    requestStart = performance.now();
    console.log(`Request to ${url} with ${updateCount} updates`);
});

LiVue.hook('request.finished', ({ url, success, error }) => {
    const duration = performance.now() - requestStart;

    if (success) {
        console.log(`Request completed in ${duration.toFixed(0)}ms`);
    } else {
        console.error('Request failed:', error);
    }
});

Analytics Integration

A practical example showing how to build a custom analytics plugin using hooks:

resources/js/analytics.js
export function initAnalytics() {
    // Track component usage
    LiVue.hook('component.init', ({ component }) => {
        analytics.track('component_mounted', {
            name: component.name,
        });
    });

    // Track server interactions
    LiVue.hook('request.started', ({ url, updateCount }) => {
        analytics.track('server_request', {
            url,
            updates: updateCount,
        });
    });

    // Track errors
    LiVue.hook('error.occurred', ({ error, componentName }) => {
        analytics.track('component_error', {
            component: componentName,
            message: error.message,
        });
    });
}

Custom Loading Indicator

Build a global loading bar that tracks all pending requests:

resources/js/app.js
let activeRequests = 0;
const loader = document.getElementById('global-loader');

LiVue.hook('request.started', () => {
    activeRequests++;
    loader.classList.remove('hidden');
});

LiVue.hook('request.finished', () => {
    activeRequests--;
    if (activeRequests === 0) {
        loader.classList.add('hidden');
    }
});