Chat Widget with Livewire 3's Persist

Image by Annie Ruygt

Fly.io can build and run your Laravel apps globally, which is especially good for Livewire! Deploy your Laravel application on Fly.io, you’ll be up and running in minutes!

We’re going to create a new app, add Livewire 3, and show how to persist a chat widget while navigating through the app.

Here’s a GitHub repository with the final code.

composer create-project laravel/laravel livewire3-persist
cd livewire3-persist

composer require livewire/livewire:^3.0@beta calebporzio/sushi

✨ We no longer need to update our layout file to include livewire JS and styles - that’ll happen automatically (unless configured not to). Note, however, that it only happens automatically on pages with certain livewire things happening on it, such as a component being used, or the new @persist option present.

We’ll create a quick layout at resources/views/components/layout.blade.php:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Video Collection</title>
    <script src="https://cdn.tailwindcss.com?plugins=typography"></script>
</head>
<body class="min-h-screen bg-gray-50 font-sans text-black antialiased">
{{ $slot }}
</body>
</html>

Then we can create a “home” page at resources/views/home.blade.php, a video list component at resources/views/components/videos.blade.php, and then a single-video page at resources/views/video.blade.php.

None of that is Livewire - plain old Laravel so far!

plain laravel app

Chat Widget

Let’s create a Livewire component - it will be a chat widget. Our goal is to persist this chat widget throughout the site without losing any state as we navigate the site.

php artisan livewire:make Chat

This creates file app/Livewire/Chat.php and resources/views/livewire/chat.blade.php.

✨ Note that components are no longer in the Http namespace!

There’s nothing fancy about this chat widget. In fact, all we’re doing for this demonstration is letting a user input their side of the conversation. Drawing the rest of the owl is your job.

What we care about is persisting the chat state.

Here’s the component:

<?php

namespace App\Livewire;

use Livewire\Component;

class Chat extends Component
{
    /**
     * @var string[]
     */
    public array $messages = [];

    public string $message = '';

    public function addMessage()
    {
        $this->messages[] = $this->message;
        $this->reset('message');
    }

    public function render()
    {
        return view('livewire.chat');
    }
}

We have an array of $messages. Anytime we addMessage(), we just append to this list of messages.

On the HTML side, we can see how addMessage() is called, providing the value $message via a form submit.

<div
    class="absolute bottom-0 right-12 h-60 w-60"
>
    <div class="w-full h-full bg-white border rounded overflow-auto flex flex-col">
        <div
            x-ref="chatBox"
            class="flex-1 p-4 text-sm flex flex-col gap-y-1">
            <div class="text-gray-400 italic">Chat history</div>
            @foreach($messages as $message)
                <div><span class="text-blue-400">You:</span> {{ $message }}</div>
            @endforeach
        </div>
        <div>
            <form
                wire:submit="addMessage"
            >
                <input 
                    wire:model="message" 
                    x-ref="messageInput" type="text" name="message" id="message" 
                    class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                />
            </form>
        </div>
    </div>
</div>

This chat widget is absolutely positioned, pinned to the bottom of the screen. Since we want it on every page, we’ll add it to our layout file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Video Collection</title>
    <script src="https://cdn.tailwindcss.com?plugins=typography,forms"></script>
</head>
<body class="min-h-screen bg-gray-50 font-sans text-black antialiased">
    <header>
        <nav class="mx-auto max-w-2xl flex items-center p-6">
          <a href="/">Home</a>
        </nav>
    </header>
    {{ $slot }}

    <livewire:chat />
</body>
</html>

We added some navigation with a home button, and included our new <livewire:chat /> component.

chat widget version 1

Fly.io ❤️ Laravel

Fly your servers close to your users—and marvel at the speed of close proximity. Deploy globally on Fly in minutes!

Deploy your Laravel app!  

And yet, it persisted

Our chat widget doesn’t persist just yet! If we add some content, but navigate away, the chat component is reloaded (as you’d expect).

To make the state persist, we need 2 new tricks in Livewire 3:

  1. wire:navigate
  2. @persist

✨ Any anchor tags <a> that we create can include the wire:navigate attribute. This puts the app into “SPA mode”, which works like Turbolinks..er…Turbo (if you’re familiar with it). It uses Javascript to load the requested page, and replaces the <body> content with the result. The browser actually thinks it’s the same page, but JS changed the <body> content, the URL, and the <title> tag. This helps us create an SPA-like experience.

✨ On top of this, we can use the new @persist blade directive! If Livewire sees a section wrapped in @persist('some-name') (and sees the named block in the result of the next request), it will keep that DOM element within the <body> tag untouched.

Here’s the updated layout file to make that work:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Video Collection</title>
    <script src="https://cdn.tailwindcss.com?plugins=typography,forms"></script>
</head>
<body class="min-h-screen bg-gray-50 font-sans text-black antialiased">
    <header>
        <nav class="mx-auto max-w-2xl flex items-center p-6">
          <a href="/" wire:navigate>Home</a>
        </nav>
    </header>
    {{ $slot }}

    @persist('chat')
    <livewire:chat />
    @endpersist
</body>
</html>

Note the addition of wire:navigate and @persist('chat'). It’s not shown in the snippet above, but I added wire:navigate to all anchor tags in this tiny app.

Now when we navigate around the app, the chat widget and its state persists!

We have an SPA without all the cruft of an SPA!

chat widget with persist