Nitro

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
Five frameworks, one page live, in this page View source ↓

The blog in numbers

Solid

Write

Svelte

On this page

React

Latest post

Blade

Oct 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

Vue

Publish, 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.

app/Nitro/Components/Blog/Together.php
<?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
{
}
resources/views/nitro/components/blog/together.blade.php
<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.

app/Nitro/Components/Blog/React/Activity.php Browser
<?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);
    }
}
resources/views/react/blog/activity.jsx
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.

app/Nitro/Components/Blog/Solid/Stats.php Browser + server
<?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]);
    }
}
resources/views/solid/blog/stats.jsx
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.

app/Nitro/Components/Blog/Blade/Latest.php Browser + server
<?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;
        }
    }
}
resources/views/nitro/components/blog/blade/latest.blade.php
<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.