Examples
All together
One blog, five frameworks on one page, composed micro-frontend style: a Svelte form, a Vue list, React activity, Solid numbers and the latest post in Blade, each its own app, talking only through events.
On this page
The blog in numbers
SolidWrite
SvelteOn this page
ReactLatest post
BladeOct 10, 2026
Shipping the new search
Search used to wait for the server on every keystroke. Now the list filters as you type, in the browser, and the server only hears about it when you save. The code didn't change much: the same PHP class, compiled to run where the user is.
Read
VuePublish, edit or delete a post, and watch the other cards follow. Every card changes at once, with no request: the events travel in the browser.
The page
Blade composes the page: each card places one component, and each component has a view in a different framework. The form and the list are the ones from Svelte and Vue blogs, unchanged.
<?php
namespace App\Nitro\Components\Blog;
use Nitro\Component;
/**
* One blog, five frameworks on one page: a Svelte form, a Vue list, React activity, Solid stats
* and the latest post in Blade. None of them knows the others; they talk through events.
*/
class Together extends Component
{
}
<div class="own-styles w-full space-y-4">
<x-blog.card title="The blog in numbers" framework="Solid" note="hears post-saved, post-deleted">
<nitro:blog.solid.stats />
</x-blog.card>
<x-blog.card title="Write" framework="Svelte" note="sends post-saved · hears edit-post">
<nitro:blog.svelte.post-form />
</x-blog.card>
<div class="grid gap-4 sm:grid-cols-2">
<x-blog.card title="On this page" framework="React" note="hears every event">
<nitro:blog.react.activity />
</x-blog.card>
<x-blog.card title="Latest post" framework="Blade" note="hears post-saved, post-deleted">
<nitro:blog.blade.latest />
</x-blog.card>
</div>
<x-blog.card title="Read" framework="Vue" note="sends edit-post, post-deleted">
<nitro:blog.vue.post-index />
</x-blog.card>
</div>
Events across frameworks
An event is sent by a component's PHP and heard by another's, whatever their views are written in. The listeners run in the browser, so every card changes the moment the event arrives.
| Event | Sent by | Heard by |
|---|---|---|
edit-post
|
the Vue list | the Svelte form, React activity |
post-saved
|
the Svelte form | the Vue list, React activity, Solid numbers, the latest post |
post-deleted
|
the Vue list | the Svelte form, React activity, Solid numbers, the latest post |
Activity, in React
It only listens: each event adds a line.
<?php
namespace App\Nitro\Components\Blog\React;
use Nitro\Attributes\On;
use Nitro\Attributes\View;
use Nitro\Component;
/** What the other components on the page say, as they say it: it listens, and sends nothing. */
#[View(react: 'blog/activity')]
class Activity extends Component
{
/** @var list<array{kind: string, text: string}> newest first */
public array $events = [];
#[On('edit-post')]
public function editing(array $post): void
{
$this->add('edit-post', "Editing “{$post['title']}”");
}
#[On('post-saved')]
public function saved(array $post, bool $created): void
{
$this->add('post-saved', ($created ? 'Published' : 'Updated')." “{$post['title']}”");
}
#[On('post-deleted')]
public function deleted(string $id): void
{
$this->add('post-deleted', 'Deleted a post');
}
public function clear(): void
{
$this->events = [];
}
private function add(string $kind, string $text): void
{
array_unshift($this->events, ['kind' => $kind, 'text' => $text]);
$this->events = array_slice($this->events, 0, 6);
}
}
const DOTS = {
'edit-post': 'bg-browser-500',
'post-saved': 'bg-emerald-500',
'post-deleted': 'bg-red-500',
};
export default function Activity({ events, clear }) {
if (events.length === 0) {
return <p className="py-6 text-center text-sm text-slate-500">Nothing yet. Publish, edit or delete a post.</p>;
}
return (
<div>
<ul className="space-y-2.5">
{events.map((event, index) => (
<li key={events.length - index} className="flex items-start gap-2.5 text-sm">
<span className={`mt-1.5 size-2 shrink-0 rounded-full ${DOTS[event.kind]}`} />
<span className="min-w-0 flex-1">
<span className="block truncate text-slate-700 dark:text-slate-300">{event.text}</span>
<code className="text-[11px] text-slate-400">{event.kind}</code>
</span>
</li>
))}
</ul>
<button type="button" className="mt-3 text-xs font-medium text-slate-500 hover:text-slate-900 dark:hover:text-white" onClick={clear}>Clear</button>
</div>
);
}
Numbers, in Solid
It counts each post's words once, from the server, and keeps the count up to date from the events.
<?php
namespace App\Nitro\Components\Blog\Solid;
use App\Blog\Posts;
use Nitro\Attributes\Computed;
use Nitro\Attributes\On;
use Nitro\Attributes\View;
use Nitro\Component;
/** The blog in numbers, kept up to date by the others' events: no request after the first. */
#[View(solid: 'blog/stats')]
class Stats extends Component
{
/** @var array<string, int> each post's words, by its id */
public array $words = [];
public function mount(): void
{
foreach (Posts::all() as $post) {
$this->words[$post['id']] = preg_match_all('/\S+/', $post['body']);
}
}
#[Computed]
public function posts(): int
{
return count($this->words);
}
#[Computed]
public function total(): int
{
return array_sum($this->words);
}
#[Computed]
public function minutes(): int
{
return max(1, (int) ceil($this->total() / 200));
}
#[On('post-saved')]
public function saved(array $post, bool $created): void
{
$this->words[$post['id']] = preg_match_all('/\S+/', $post['body']);
}
#[On('post-deleted')]
public function deleted(string $id): void
{
unset($this->words[$id]);
}
}
export default function Stats(props) {
const figures = () => [
{ label: 'Posts', value: props.posts },
{ label: 'Words', value: props.total.toLocaleString() },
{ label: 'To read them all', value: `${props.minutes} min` },
];
return (
<dl class="grid grid-cols-3 divide-x divide-slate-200 dark:divide-white/10">
{figures().map((figure) => (
<div class="px-4 first:pl-0">
<dt class="text-xs text-slate-500">{figure.label}</dt>
<dd class="mt-1 text-2xl font-semibold tracking-tight text-slate-900 tabular-nums dark:text-white">{figure.value}</dd>
</div>
))}
</dl>
);
}
The latest post, in Blade
It shows the newest post, and follows the form and the list as they publish, change and delete.
<?php
namespace App\Nitro\Components\Blog\Blade;
use App\Blog\Posts;
use Nitro\Attributes\On;
use Nitro\Component;
/** The newest post, kept up to date by the others' events. */
class Latest extends Component
{
/** @var array{id: string, title: string, body: string, date: string}|null */
public ?array $post = null;
public function mount(): void
{
$this->post = Posts::all()[0] ?? null;
}
#[On('post-saved')]
public function saved(array $post, bool $created): void
{
if ($created || ($this->post !== null && $this->post['id'] === $post['id'])) {
$this->post = $post;
}
}
#[On('post-deleted')]
public function deleted(string $id): void
{
if ($this->post !== null && $this->post['id'] === $id) {
$this->post = null;
}
}
}
<div>
@if ($post === null)
<p class="py-6 text-center text-sm text-slate-500">Deleted. Publish another and it shows here.</p>
@else
<p class="text-xs text-slate-500">{{ $post['date'] }}</p>
<p class="mt-1 truncate font-semibold text-slate-900 dark:text-white">{{ $post['title'] }}</p>
<p class="mt-1 line-clamp-3 text-sm leading-6 whitespace-pre-line text-slate-600 dark:text-slate-400">{{ $post['body'] }}</p>
@endif
</div>
Events reach the components on one page. To reach other tabs or other people, broadcast with Laravel
(Reverb or Pusher) and listen with #[On('echo:...')]: see
Events.