Using laravel-echo-server with React Native

Dec 3, 2019

Let’s build a proof-of-concept chat app using the following technologies:

Laravel: Simple chat backend

Our Proof-of-concept will simply broadcast a new chat message to all users of a public chat room.

composer create-project --prefer-dist laravel/laravel chat-server "5.8.*"

Eloquent models

php artisan make:model Chat --migration
// chat-server/app/Chat.php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Chat extends Model
{
    //
}

// chat-server/database/migrations/2019_11_27_104925_create_chats_table.php

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

class CreateChatsTable extends Migration
{
    public function up()
    {
        Schema::create('chats', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('chats');
    }
}
php artisan make:model ChatMessage --migration
// chat-server/app/ChatMessage.php
namespace App;

use Illuminate\Database\Eloquent\Model;

class ChatMessage extends Model
{
    protected $visible = ['id', 'text'];

    protected $fillable = ['chat_id', 'user_id', 'text'];

    public function chat()
    {
        return $this->belongsTo(Chat::class);
    }
}

// chat-server/database/migrations/2019_11_27_104937_create_chat_messages_table.php

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

class CreateChatMessagesTable extends Migration
{
    public function up()
    {
        Schema::create('chat_messages', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('text');
            $table->unsignedBigInteger('chat_id');
            $table->unsignedBigInteger('user_id');
            $table->timestamps();

            $table
                ->foreign('chat_id')
                ->references('id')
                ->on('chats');

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

    public function down()
    {
        Schema::dropIfExists('chat_messages');
    }
}
php artisan make:migration create_chat_user_table --create=chat_user
// chat-server/database/migrations/2019_11_27_104948_create_chat_user_table.php

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

class CreateChatUserTable extends Migration
{
    public function up()
    {
        Schema::create('chat_user', function (Blueprint $table) {
            $table->unsignedBigInteger('chat_id');
            $table->unsignedBigInteger('user_id');
            $table->primary(['chat_id', 'user_id']);

            $table
                ->foreign('chat_id')
                ->references('id')
                ->on('chats')
                ->onDelete('cascade');

            $table
                ->foreign('user_id')
                ->references('id')
                ->on('users')
                ->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::dropIfExists('chat_user');
    }
}

ChatMessageObserver

php artisan make:observer ChatMessageObserver --model=ChatMessage

It“s a good idea to put side effects in Model Observers. broadcast is a good example of a side effect: Regardless of how and when a ChatMessage has been created, we want to broadcast the associated event.

// chat-server/app/Observers/ChatMessageObserver.php

namespace App\Observers;

use App\ChatMessage;
use App\Events\ChatMessageCreated as ChatMessageCreatedEvent;

class ChatMessageObserver
{
    public function created(ChatMessage $message)
    {
        broadcast(new ChatMessageCreatedEvent($message));
    }
}

// chat-server/app/Providers/AppServiceProvider.php

    public function boot()
    {
        ChatMessage::observe(ChatMessageObserver::class);
    }

ChatMessageCreated Event

php artisan make:event ChatMessageCreated

Our “chat message created” event will be broadcasted on a simple open/public channel.

Note

Don“t forget to add implements ShouldBroadcast to your Event class!

// chat-server/app/Events/ChatMessageCreated.php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

use App\ChatMessage;

class ChatMessageCreated implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;

    public function __construct(ChatMessage $message)
    {
        $this->message = $message;
    }

    public function broadcastOn()
    {
        return new Channel('chats.' . $this->message->chat->id);
    }
}

Chat public channel

Since our simple example does not require channel authentication/authorization, we do not need to declare our public chat channel in routes/channels.php file.

This is not quite a real-world example, since literally anybody can join the chats.* channels.

Achieving channel authentication & authorization via Laravel Passport will be part of another post.

React Native: Receiving broadcast

Quickly setup a new React Native app:

react-native init ChatClient && cd ChatClient/
yarn add laravel-echo socket.io-client

Let“s configure our socket directly in the App.js file:

// ChatClient/App.js

import Echo from 'laravel-echo';
import socketio from 'socket.io-client';

const echo = new Echo({
  host: 'http://127.0.0.1:6001',
  broadcaster: 'socket.io',
  client: socketio,
});

echo
  .channel('chats.1')
  .listen('ChatMessageCreated', ev => console.log(ev.message.text));

const App: () => React$Node = () => {
  // ...

Note

chats.1 will be seeded in next section.

Running the demo

Redis broadcaster

We will not use the default Pusher broadcast driver. We want to use a 100% free solution based on open source software:

  • Redis as our Laravel broadcaster
  • laravel-echo-server as our Socket.IO server (this listens to our Redis broadcaster)
  • Echo+Socket.IO as our React Native Socket.IO client

Follow the official documentation for configuring broadcasting, as well as Redis, but in a nutshell:

  • Uncomment BroadcastServiceProvider in config/app.php
  • $ composer require predis/predis
  • Set “redis” as your BROADCAST_DRIVER in your local .env file

Note

Don“t forget to actually install & start Redis on your machine.

Queue listener

As you may have read in the documentation, we need to setup a queue listener for Laravel to broadcast our application events.

All event broadcasting is done via queued jobs so that the response time of your application is not seriously affected.

In a nutshell:

  • Leave the defaults in config/database.php redis.default
  • Set “redis” as your QUEUE_CONNECTION in your local .env file

laravel-echo-server

laravel-echo-server is a Socket.IO server compatible with Laravel Echo. This is where our ChatClient app will connect in order to receive updates from the Laravel backend.

yarn global add laravel-echo-server

As per the official documentation, let“s generate the socket server configuration file, just accept all the defaults, we will be editing the file manually later on.

laravel-echo-server init

Note

Adjust manually devMode setting to true since default value is false.

Since we won“t be using the light http API of the socket server, we don“t need to generate a client ID & secret.

Starting from Laravel 5.8.\*, Laravel events are namespaced in Redis (certainly a good idea) via a new “prefix” option that can be configured to your liking:

// config/database.php (dot notation)

'redis.options.prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_')

As you can see, by default, the namespace/prefix is laravel_database_.

laravel-echo-server default configuration does not play well with this new setting. We need to explicitly specify the keyPrefix in the socket server config:

// laravel-echo-server.json (dot notation)

"databaseConfig.redis.keyPrefix": "laravel_database_"

Seeding the database

Since creating new chats/users is out of scope, let“s just seed the database manually for the demo.

php artisan migrate
php artisan tinker

>>> User::create([ 'name' => 'John', 'email' => '[email protected]', 'password' => Hash::make('john_pass') ]);
>>> Chat::create();
>>> DB::table('chat_user')->insert([ ['chat_id' => 1, 'user_id' => 1]]);

Note

We“ve only created one user being alone in a chat room. We“re just going to assume that there are more users listening to this chat room.

Run !

Quite a bunch of stuff to run.

php artisan serve
php artisan queue:work
laravel-echo-server start
react-native run-ios

Now let“s create a ChatMessage in our public chat room.

php artisan tinker

>>> ChatMessage::create(['chat_id' => 1, 'user_id' => 1, 'text' => 'Hello World']);

If everything goes well, you should see “Hello World” printed in the Developer console of your React Native app!

If you enjoyed this article, give it a clap on Medium.