Building A Real-Time Chat App Using Laravel 11 (Breeze, Reverb, React Js)

Building A Real-Time Chat App Using Laravel 11 (Breeze, Reverb, React Js)

Avatar photoPosted by

Hi! Today, we will learn how to build a real-time chat app using Laravel in minutes. We will use the packages Laravel has. Before that, let’s have a discussion about what we are using in this tutorial.

Laravel is a free, open-source PHP Web Framework intended for the development of web applications following the MVC (Model-View-Controller) architectural pattern. Laravel is designed to make developing web apps faster and easier by using built-in features.

Laravel Breeze gives you a simple and minimal implementation of login, registration, password reset, email verification, and password confirmation which are part of Laravel’s authentication features.

React or also called React.js or Reactjs is a free and open-source JavaScript library used for building user interfaces(UI). It is one of the most popular JavaScript libraries for building the front end. React is created by Facebook and maintained by Facebook.

Laravel Reverb is a first-party WebSocket server for Laravel applications. It brings real-time communication between the client and server directly to your Laravel projects

Prerequisite:

Step 1: Install Laravel 11

First, select a folder you want Laravel to be installed then execute this command on Terminal or CMD to install Laravel 11:

Install via composer:

composer create-project laravel/laravel laravel-11-real-time-chat-app

Install via Laravel Installer:

laravel new laravel-11-real-time-chat-app

Step 2: Set Database Configuration

Open the .env file and set the database configuration:

.env

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your database name(laravel_11_real_time_chat_app)
DB_USERNAME=your database username(root)
DB_PASSWORD=your database password(root)

Step 3: Install Laravel Breeze

Then install the Laravel Breeze package

composer require laravel/breeze --dev

After installing packages, run the artisan command breeze:install to publish the authentication views, routes, controller, and other resources to the project.

php artisan breeze:install

We will be choosing React with Inertia as stack:

image 66 Binaryboxtuts

After the installation, run these commands to run the migration and compile the assets.

php artisan migrate
npm install
npm run dev

Step 4: Run the App

Run the laravel app:

php artisan serve

You can now navigate to the login and register URL.

http://localhost:8000/login
http://localhost:8000/register

Now register these 3 accounts, we will be using these accounts for the real-time chat.

http://localhost:8000/register

image 63 Binaryboxtuts
image 64 Binaryboxtuts
image 65 Binaryboxtuts

Step 5: Install Laravel Reverb

Install Laravel Reverb by running this command:

php artisan install:broadcasting

This command will set up Reverb with a sensible set of default configuration options.

Step 6: Start the Reverb Server

start the Reverb server with this command:

php artisan reverb:start

By default, the server runs on port 8080. You can specify a custom host or port using the --host and --port options if needed eg. php artisan reverb:start --host=127.0.0.1 --port=9000 .

Step 7: Create a Model with Migration

A model is a class that represents a table on a database.

Migration is like a version of your database.

Run this command on Terminal or CMD:

php artisan make:model Message --migration

After running this command you will find a file in this path “database/migrations” and update the code in that file.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('messages', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('sender_id');
            $table->unsignedBigInteger('recipient_id');
            $table->text('message');
            $table->timestamps();

            $table->foreign('sender_id')->references('id')->on('users');
            $table->foreign('recipient_id')->references('id')->on('users');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('messages');
    }
};

Run the migration:

php artisan migrate

Step 8: Create an Event

An event is a way to signal that something has happened within your application. It allows you to decouple various parts of your system by letting different parts respond to the same event in their own way. We will now create a MessageSent event class for our message broadcasting and we will use the ShouldBroadcastNow interface to immediately broadcast an event instead of queuing it for later processing. This is especially useful when you need real-time updates without any delay.

php artisan make:event MessageSent

update MessageSent.php

<?php

namespace App\Events;

use App\Models\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageSent implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     */
    public function __construct(public Message $message)
    {
        //
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel("message.{$this->message->recipient_id}"),
        ];
    }
}

Step 9: Create A Private Channel

The channels.php is a configuration file used for defining the channels your application supports for broadcasting events. This file is located in the routes directory and plays a crucial role in setting up WebSocket channels, both public and private, for real-time event broadcasting.

  • Public Channels: Accessible to anyone.
  • Private Channels: Require authentication to join.

We will now add a private channel for the chat app:

channels.php

<?php

use Illuminate\Support\Facades\Broadcast;

// Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
//     return (int) $user->id === (int) $id;
// });

Broadcast::channel('message.{id}', function ($user, $id) {
    return (int) $user->id === (int) $id;
});

Step 10: Create Controller

We will now create to handle our messaging API. Run this command to create a controller:

php artisan make:controller MessageController

Add these lines of code:

<?php

namespace App\Http\Controllers;

use App\Events\MessageSent;
use App\Models\Message;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;

class MessageController extends Controller
{

    public function inbox() {
        $users = User::where('id', '!=', Auth::user()->id)->get();
        return Inertia::render('Inbox', ['users' => $users]);
    }

    public function store(Request $request, User $user) {
        $message = new Message;
        $message->sender_id = Auth::user()->id;
        $message->recipient_id = $user->id;
        $message->message = $request->message;
        $message->save();

        broadcast(new MessageSent($message));

        return response()->json($message);
    }

    public function show(User $user) {
        $user1Id = Auth::user()->id;
        $user2Id = $user->id;

        $messages = Message::where(function ($query) use ($user1Id, $user2Id) {
            $query->where('sender_id', $user1Id)
                  ->where('recipient_id', $user2Id);
        })
        ->orWhere(function ($query) use ($user1Id, $user2Id) {
            $query->where('sender_id', $user2Id)
                  ->where('recipient_id', $user1Id);
        })
        ->orderBy('created_at', 'asc')
        ->get(); 

        return response()->json($messages);
    }
}

Step 11: Define Routes

Update web.php to include routes for the MessageController:

web.php

<?php

use App\Http\Controllers\MessageController;
use App\Http\Controllers\ProfileController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});

Route::get('/dashboard', function () {
    return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');

    Route::get('/inbox', [MessageController::class, 'inbox'])->name('inbox');
    Route::post('/message/{user}', [MessageController::class, 'store'])->name('message.store');
    Route::get('/message/{user}', [MessageController::class, 'show'])->name('message.show');
});

require __DIR__.'/auth.php';

Step 12: Create Inbox Page And Update AuthenticatedLayout

We will now create a react component for our inbox page. Create a file inside resources\js\Pages\, the filename is Inbox.jsx . And add these lines of codes:

resources\js\Pages\Inbox.jsx

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, useForm } from '@inertiajs/react';
import axios from 'axios';
import { useEffect, useRef, useState } from 'react';

export default function Inbox({ auth, users }) {

    const webSocketChannel = `message.${auth.user.id}`;

    const [selectedUser, setSelectedUser] = useState(null)
    const [currentMessages, setCurrentMessages] = useState([])
    const [messageInput, setMessageInput] = useState("")

    const targetScrollRef = useRef(null);
    const selectedUserRef = useRef(null)

    const scrollToBottom = () => {
        if(targetScrollRef.current) {
            targetScrollRef.current.scrollIntoView({ behavior: "smooth" });
        }
    };

    const sendMessage = async () => {
        await axios.post(`/message/${selectedUserRef.current.id}`, {message:messageInput});
        setMessageInput('')
        getMessages()
    }

    const getMessages = async () => {
        const response = await axios.get(`/message/${selectedUserRef.current.id}`);
        setCurrentMessages(response.data)
    }

    useEffect(()=>{
        selectedUserRef.current = selectedUser
        if(selectedUser) {
            getMessages()
        }
    },[selectedUser])

    useEffect(()=>{
        setTimeout(() => {
            scrollToBottom()
        }, 100);
    },[currentMessages])

    const connectWebSocket = () => {
        window.Echo.private(webSocketChannel)
            .listen('MessageSent', async (e) => {
                await getMessages();
            });
    }
    useEffect(()=>{
        connectWebSocket();

        return () => {
            window.Echo.leave(webSocketChannel);
        }
    },[])

    return (
        <AuthenticatedLayout>
            <Head title="Inbox" />

            <div className="h-screen flex bg-gray-100" style={{height:'90vh'}}>
                {/* Sidebar */}
                <div className="w-1/4 bg-white border-r border-gray-200">
                    <div className="p-4 bg-gray-100 font-bold text-lg border-b border-gray-200">
                        Inbox
                    </div>
                    <div className="p-4 space-y-4">
                    {/* Contact List */}
                    {users.map((user, key) => (
                        <div
                        key={key}
                        onClick={()=>setSelectedUser(user)}
                        className={`flex items-center ${user.id == selectedUser?.id ? 'bg-blue-500 text-white' : ''} p-2 hover:bg-blue-500 hover:text-white rounded cursor-pointer`}
                        >
                        <div className="w-12 h-12 bg-blue-200 rounded-full"></div>
                        <div className="ml-4">
                            <div className="font-semibold">{user.name}</div>
                        </div>
                        </div>
                    ))}
                    </div>
                </div>

                {/* Chat Area */}
                <div className="flex flex-col w-3/4">
                    {!selectedUser &&
                        <div className=' h-full flex justify-center items-center text-gray-800 font-bold'>
                            Select Conversation
                        </div>
                    }
                    {selectedUser &&
                        <>
                        {/* Chat Header */}
                        <div className="p-4 border-b border-gray-200 flex items-center">
                            <div className="w-12 h-12 bg-blue-200 rounded-full"></div>
                            <div className="ml-4">
                                <div className="font-bold">{selectedUser?.name}</div>
                            </div>
                        </div>

                        {/* Chat Messages */}
                        <div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
                            {currentMessages.map((message, index) => (
                                <div
                                    key={index}
                                    className={`flex ${
                                        message.sender_id  == auth.user.id ? "justify-end" : "justify-start"
                                    }`}
                                >
        
                                    <div
                                        className={`${
                                            message.recipient_id  == auth.user.id
                                            ? "bg-gray-200 text-gray-800"
                                            : "bg-blue-500 text-white"
                                        } p-3 rounded-lg max-w-xs`}
                                    >
                                        {message.message}
                                    </div>
                                </div>
                            ))}
                            <span ref={targetScrollRef}></span>
                        </div>

                        {/* Message Input */}
                        <div className="p-4 bg-white border-t border-gray-200">
                            <div className="flex items-center">
                                <input
                                    type="text"
                                    placeholder="Type a message..."
                                    className="flex-1 p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                                    value={messageInput}
                                    onChange={(e)=>setMessageInput(e.target.value)}
                                />
                                <button 
                                    onClick={sendMessage}
                                    className="ml-4 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
                                    Send
                                </button>
                            </div>
                        </div>
                        </>
                    }
                    
                </div>
            </div>
        </AuthenticatedLayout>
    );
}

resources\js\Layouts\AuthenticatedLayout.jsx

import ApplicationLogo from '@/Components/ApplicationLogo';
import Dropdown from '@/Components/Dropdown';
import NavLink from '@/Components/NavLink';
import ResponsiveNavLink from '@/Components/ResponsiveNavLink';
import { Link, usePage } from '@inertiajs/react';
import { useState } from 'react';

export default function AuthenticatedLayout({ header, children }) {
    const user = usePage().props.auth.user;

    const [showingNavigationDropdown, setShowingNavigationDropdown] =
        useState(false);

    return (
        <div className="min-h-screen bg-gray-100">
            <nav className="border-b border-gray-100 bg-white">
                <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
                    <div className="flex h-16 justify-between">
                        <div className="flex">
                            <div className="flex shrink-0 items-center">
                                <Link href="/">
                                    <ApplicationLogo className="block h-9 w-auto fill-current text-gray-800" />
                                </Link>
                            </div>

                            <div className="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
                                <NavLink
                                    href={route('dashboard')}
                                    active={route().current('dashboard')}
                                >
                                    Dashboard
                                </NavLink>
                            </div>
                            <div className="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
                                <NavLink
                                    href={route('inbox')}
                                    active={route().current('inbox')}
                                >
                                    Inbox
                                </NavLink>
                            </div>
                        </div>

                        <div className="hidden sm:ms-6 sm:flex sm:items-center">
                            <div className="relative ms-3">
                                <Dropdown>
                                    <Dropdown.Trigger>
                                        <span className="inline-flex rounded-md">
                                            <button
                                                type="button"
                                                className="inline-flex items-center rounded-md border border-transparent bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-500 transition duration-150 ease-in-out hover:text-gray-700 focus:outline-none"
                                            >
                                                {user.name}

                                                <svg
                                                    className="-me-0.5 ms-2 h-4 w-4"
                                                    xmlns="http://www.w3.org/2000/svg"
                                                    viewBox="0 0 20 20"
                                                    fill="currentColor"
                                                >
                                                    <path
                                                        fillRule="evenodd"
                                                        d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
                                                        clipRule="evenodd"
                                                    />
                                                </svg>
                                            </button>
                                        </span>
                                    </Dropdown.Trigger>

                                    <Dropdown.Content>
                                        <Dropdown.Link
                                            href={route('profile.edit')}
                                        >
                                            Profile
                                        </Dropdown.Link>
                                        <Dropdown.Link
                                            href={route('logout')}
                                            method="post"
                                            as="button"
                                        >
                                            Log Out
                                        </Dropdown.Link>
                                    </Dropdown.Content>
                                </Dropdown>
                            </div>
                        </div>

                        <div className="-me-2 flex items-center sm:hidden">
                            <button
                                onClick={() =>
                                    setShowingNavigationDropdown(
                                        (previousState) => !previousState,
                                    )
                                }
                                className="inline-flex items-center justify-center rounded-md p-2 text-gray-400 transition duration-150 ease-in-out hover:bg-gray-100 hover:text-gray-500 focus:bg-gray-100 focus:text-gray-500 focus:outline-none"
                            >
                                <svg
                                    className="h-6 w-6"
                                    stroke="currentColor"
                                    fill="none"
                                    viewBox="0 0 24 24"
                                >
                                    <path
                                        className={
                                            !showingNavigationDropdown
                                                ? 'inline-flex'
                                                : 'hidden'
                                        }
                                        strokeLinecap="round"
                                        strokeLinejoin="round"
                                        strokeWidth="2"
                                        d="M4 6h16M4 12h16M4 18h16"
                                    />
                                    <path
                                        className={
                                            showingNavigationDropdown
                                                ? 'inline-flex'
                                                : 'hidden'
                                        }
                                        strokeLinecap="round"
                                        strokeLinejoin="round"
                                        strokeWidth="2"
                                        d="M6 18L18 6M6 6l12 12"
                                    />
                                </svg>
                            </button>
                        </div>
                    </div>
                </div>

                <div
                    className={
                        (showingNavigationDropdown ? 'block' : 'hidden') +
                        ' sm:hidden'
                    }
                >
                    <div className="space-y-1 pb-3 pt-2">
                        <ResponsiveNavLink
                            href={route('dashboard')}
                            active={route().current('dashboard')}
                        >
                            Dashboard
                        </ResponsiveNavLink>
                    </div>

                    <div className="border-t border-gray-200 pb-1 pt-4">
                        <div className="px-4">
                            <div className="text-base font-medium text-gray-800">
                                {user.name}
                            </div>
                            <div className="text-sm font-medium text-gray-500">
                                {user.email}
                            </div>
                        </div>

                        <div className="mt-3 space-y-1">
                            <ResponsiveNavLink href={route('profile.edit')}>
                                Profile
                            </ResponsiveNavLink>
                            <ResponsiveNavLink
                                method="post"
                                href={route('logout')}
                                as="button"
                            >
                                Log Out
                            </ResponsiveNavLink>
                        </div>
                    </div>
                </div>
            </nav>

            {header && (
                <header className="bg-white shadow">
                    <div className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
                        {header}
                    </div>
                </header>
            )}

            <main>{children}</main>
        </div>
    );
}

Step 13: Run The App

These are the commands you need to rerun your app. Since we already run this command in our previous step, there is no need to run the commands below.

Starts the development server of the Laravel application:

php artisan serve

Compile and build frontend assets:

npm run dev

Start Laravel Reverb WebSocket server:

php artisan reverb:start

Now test your app:

http://127.0.0.1:8000

http://127.0.0.1:8000/inbox

image 67 Binaryboxtuts