Real-Time Models
An island can keep a model in sync with the server. The model broadcasts its lifecycle onto a private channel, the page hands the model to the island, and useModel() reconciles the component's state whenever an event arrives — in every open tab, for every user allowed on the channel.
Prerequisites
- Broadcasting configured with a driver (
reverb,pusher,ably). With thenullorlogdriver, or none at all, the trait below stays quiet and the island renders as a static one. - Laravel Echo initialised on the page and assigned to
window.Echo, as the default Laravelbootstrap.jsdoes.
Preparing the Model
<?php
declare(strict_types=1);
namespace App\Models;
use Aaix\LaravelIslands\Concerns\InteractsWithIslands;
use Illuminate\Database\Eloquent\Model;
class ShopOrder extends Model
{
use InteractsWithIslands;
}The trait composes Laravel's own BroadcastsEvents and adds three things:
- A channel name.
islandChannel()returns the class with backslashes replaced by dots plus the key:App.Models.ShopOrder.42.<x-island>writes the same name into the payload, so the two sides never disagree. - Automatic events.
created,updatedanddeletedbroadcast on that private channel asShopOrderCreated,ShopOrderUpdatedandShopOrderDeleted, carrying the model's array form undermodel. - A safety net. When broadcasting is unusable — no driver, or the driver throws — the event is skipped with a warning in the log instead of failing the request that saved the model.
Override islandChannel() when a model should share a channel with something else, for example its parent:
public function islandChannel(): string
{
return 'App.Models.ShopOrder.'.$this->shop_order_id;
}Authorizing the Channel
The channel is private. Authorize it in routes/channels.php as usual:
use App\Models\ShopOrder;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('App.Models.ShopOrder.{id}', function (User $user, int $id) {
return $user->can('view', ShopOrder::findOrFail($id));
});Subscribing From the Page
Hand the model to the tag:
<x-island name="OrderView" :subscribe="$order" />A single model is stored under its class name in camelCase — shopOrder for ShopOrder. Pass an array to choose the keys yourself, or to subscribe to several models:
<x-island
name="OrderView"
:subscribe="['order' => $order, 'customer' => $order->customer]"
/>For each subscription, the array form of the model is added to the props under the same key — unless you passed a prop of that name, in which case yours wins. That is how an island starts with the presented shape and still receives updates for it.
Reading the Model in the Component
<script setup>
import { useModel } from '@aaix/laravel-islands/vue';
const { data: order, isDeleted } = useModel('order');
</script>
<template>
<p v-if="isDeleted">{{ t('This order has been deleted.') }}</p>
<div v-else>{{ order.number }} — {{ order.status }}</div>
</template>data starts as the prop under that key. On mount, the composable joins the channel; on an updated event it merges the broadcast payload into data; on deleted it sets isDeleted. The channel is left when the component unmounts.
Reloading Instead of Merging
The broadcast carries the model's raw attributes, which is rarely the shape a presenter produced. Two options change what happens on an update:
const { data: order } = useModel('order', {
refetch: async () => {
const response = await fetch(props.orderUrl, { headers: { Accept: 'application/json' } });
return (await response.json()).data;
},
});refetch ignores the payload and reloads from your own endpoint; whatever it resolves becomes data. For full control, onUpdate receives the event and the ref:
useModel('order', {
onUpdate(event, data) {
data.value = { ...data.value, status: event.model.status };
},
});onUpdate takes precedence over refetch; without either, the payload is merged.
Listening to Other Events
useEcho().privateChannel(name) joins a channel that is not a model — an import's progress, a print queue — and leaves it again when the component unmounts:
const { privateChannel } = useEcho();
privateChannel('imports.42').listen('ImportProgressed', (event) => progress.value = event.percent);When Echo Is Absent
useModel() and useEcho() check for window.Echo at mount time. Without it, they log [islands] window.Echo is not initialised — real-time disabled once and return the initial data unchanged. An island therefore never depends on a websocket to render; it only gets better with one.