Build WebRTC chat with React.js

In this article I’m going to share how could be build WebRTC chat with React.js. Before we continue lets describe briefly what React.js and WebRTC are.

The application from this tutorial is available at GitHub.

React.js

React.js is reactive JavaScript framework, which helps you to build user interface. Facebook states that we can think have React as the “V” in MVC. React’s main aspect is the state. When the state of the application changes this automatically propagates through the application’s components. A React component is a self-contained module, which is composed by one or more other components. Usually the component depends on state, which is being provided by a parent component. May be the explanation seems quite abstract now, but during the tutorial the picture will get much more clear.

WebRTC

RTC stands for Real-Time Communication. Until browsers implemented WebRTC our only way to provide communication between several browsers was to proxy the messages via a server between them (using WebSockets or HTTP). WebRTC makes the peer-to-peer communication between browsers possible. Using the NAT traversal framework — ICE, we are able find the most appropriate route between the browsers and make them communicate without mediator. Since 1st of July 2014, v1.0 of the WebRTC browser APIs standard is already published by W3C.

NAT

Before we continue with the tutorial, let’s say a few words about what NAT is. NAT stands for Network Address Translation. It is quite common way for translating internal (private) IP addresses to public ones and vice versa. A lot of ISP providers with limited capacity of public IP addresses uses this way of scaling using private IP addresses in their internal networks and translating them to public addresses visible to the outside world. More about NAT and the different types of NAT could be read in this wiki article

Implementation

Now let’s get starting with the actual implementation of our WebRTC based chat.

Architecture

High-level overview

Since I’m kind of traditionalist I’ll start by providing a basic, high-level overview of the architecture of our p2p (peer to peer) chat.

The dashed arrows, in the diagram, indicate signaling WebSocket connections. Each client initiates such connection with the server. With these connections each client aims to register itself on the server and use the server as a proxy during the NAT traversal procedures, defined by the signaling protocol (for now we can think of the signaling protocol as SIP or XMPP Jingle). Actually the signaling protocol in our case is provided by Peer.js.

The solid arrow stands for peer-to-peer TCP or UDP (TCP in our case) data channel between the browsers. We use full mesh, which scales badly especially when we use video or audio streaming. For the purpose of our chat full mesh is good enough.

Low-level overview

In the beginning of the blog post I mentioned that React.js application contains a finite amount of React.js components composed together. In this subsection I’ll illustrate, which are the different components of our application and how they are composed together. The diagram below isn’t following the UML standard, it only illustrate, as clearly as possible, our micro-architecture.

Let’s concentrate on the left-hand side of the diagram. As you see we have a set of nested components. The most outer, non-named, component (the rectangle, which contains all other rectangles), is the ChatBox component. In its left-hand side is positioned the MessagesList component, which is composition of ChatMessage components. Each ChatMessage component contains a different chat message, which has author, date when published and content. On the right-hand side of the ChatBox is positioned the UsersList component. This component lists all users, which are currently in the chat session. The last component is the MessageInput component. The MessageInput component is a simple text input, which once detect a press of the Enter key triggers an event, with data – its value.

The ChatBox component uses ChatProxy. The ChatProxy is responsible for registering the current client on the server and talking with the other peers. For simplicity I’ve used Peer.js, which provides nice high-level API, wrapping the browser’s WebRTC API.

Getting started

In this section we are going to setup our project…

Create a directory called react-p2p and enter it:

mkdir react-p2p && cd react-p2p

Create a package.json file with the content:

{
  “name”: “react-peerjs”,
  “version”: “0.0.0”,
  “description”: “ReactJS chat with PeerJS”,
  “main”: “index.js”,
  “scripts”: {
    “test”: “”
  },
  “keywords”: [
    “webrtc”,
    “nodejs”,
    “react”,
    “javascript”,
    “awesome”
  ],
  “author”: “mgechev”,
  “license”: “MIT”,
  “dependencies”: {
    “express”: “~4.8.4”,
    “peer”: “~0.2.6”,
    “socket.io”: “~1.0.6”
  }
}

This file defines primitive information for our server, like name, version, keywords and dependencies. The dependencies of our server are:

  • express – we are going to use express as a static server
  • peer – A server, which implements the signaling of our application
  • io

Now let’s take a look at ./bower.json:

{
  “name”: “react-peerjs”,
  “main”: “index.js”,
  “version”: “0.0.0”,
  “authors”: [
    “mgechev”
  ],
  “license”: “MIT”,
  “ignore”: [
    “**/.*”,
    “node_modules”,
    “bower_components”,
    “public/lib”,
    “test”,
    “tests”
  ],
  “dependencies”: {
    “react”: “~0.11.1”,
    “jquery”: “~2.1.1”,
    “bootstrap”: “~3.2.0”,
    “eventEmitter”: “~4.2.7”,
    “peerjs”: “~0.3.9”
  }
}

The bower.json file defines primitive information and dependencies for the client-side of the application. The required dependencies are:

  • React – the framework, we are going to use for building our UI.
  • eventEmitter – our components are going to fire events, which later are going to be handled by other components. EventEmitter will be used as based class for our “event-driven” components.
  • peerjs – wraps the browser’s WebRTC API into high-level, easier to use API.
  • jquery
  • bootstrap

And… .bowerrc

{
  “directory”: “public/lib”
}

In .bowerrc we define that we want all bower dependencies to be saved at /public/lib.

Now, in order to resolve all dependencies, run:

bower install && npm install

Now let’s start with our implementation.

Server-side

We have a few lines of Node.js, which are required for signaling and establishing p2p connection between the peers.

Create a file called index.js in the root of our application and add the following content:

var PeerServer = require(‘peer’).PeerServer,
    express = require(‘express’),
    Topics = require(‘./public/src/Topics.js’),
    app = express(),
    port = process.env.PORT || 3001;app.use(express.static(__dirname + ‘/public’));var expressServer = app.listen(port);
var io = require(‘socket.io’).listen(expressServer);console.log(‘Listening on port’, port);var peerServer = new PeerServer({ port: 9000, path: ‘/chat’ });peerServer.on(‘connection’, function (id) {
  io.emit(Topics.USER_CONNECTED, id);
  console.log(‘User connected with #’, id);
});peerServer.on(‘disconnect’, function (id) {
  io.emit(Topics.USER_DISCONNECTED, id);
  console.log(‘User disconnected with #’, id);
});

In the snippet above, we create a simple express server, which servers static files from the directory /public, located in the root folder. After that we create a PeerServer, which on the other hand is responsible for handling the signaling between the different peers. In our case we can think of the PeerServer and the protocol, which it implements as alternative of SIP or XMPP Jingle.

Once our PeerServer detects that a peer has been connected to it, it triggers the event USER_CONNECTED to all peers. Once a client disconnects from the PeerServer we trigger USER_DISCONNECTED. These two events are very important for handling the list of currently available users.

Client-side

ChatProxy.js

Now let’s take a look at the component responsible for communication between our peers and registering them on the server.

The biggest advantage of putting the logic for p2p communication and signaling out of the react components is achieving separation of concerns. This way we achieve highly coherent components, which are reusable and testable.

Inside /public/src/models/ create a file called ChatProxy.js.

function ChatProxy() {
  EventEmitter.call(this);
  this._peers = {};
}ChatProxy.prototype = Object.create(EventEmitter.prototype);

Our ChatProxy extends EventEmitter. We use inheritance because we want to reuse the functionality provided by the EventEmitter and fire events when we receive new message, client connects or disconnects.

The most complex method, which ChatProxy implements is the connect method. Let’s take a look at it:

ChatProxy.prototype.connect = function (username) {
  var self = this;
  this.setUsername(username);
  this.socket = io();
  this.socket.on(‘connect’, function () {
    self.socket.on(Topics.USER_CONNECTED, function (userId) {
      if (userId === self.getUsername()) {
        return;
      }
      self._connectTo(userId);
      self.emit(Topics.USER_CONNECTED, userId);
      console.log(‘User connected’, userId);
    });
    self.socket.on(Topics.USER_DISCONNECTED, function (userId) {
      if (userId === self.getUsername()) {
        return;
      }
      self._disconnectFrom(userId);
      self.emit(Topics.USER_DISCONNECTED, userId);
      console.log(‘User disconnected’, userId);
    });
  });
  console.log(‘Connecting with username’, username);
  this.peer = new Peer(username, {
    host: location.hostname, port: 9000, path: ‘/chat’
  });
  this.peer.on(‘open’, function (userId) {
    self.setUsername(userId);
  });
  this.peer.on(‘connection’, function (conn) {
    self._registerPeer(conn.peer, conn);
    self.emit(Topics.USER_CONNECTED, conn.peer);
  });
};

If the client have passed username to the connect call we set the current username, after that with io() we establish new socket.io connection. The socket.io connection is going to be used for receiving USER_CONNECTED and USER_DISCONNECTED events. Once we have been connected to the socket.io server, we bind to these events. We need extra, socket.io, connection here because the API of Peer.js doesn’t provide all required events by its public API.

In the snippet:

self.socket.on(Topics.USER_CONNECTED, function (userId) {

  if (userId === self.getUsername()) {
    return;
  }
  self._connectTo(userId);
  self.emit(Topics.USER_CONNECTED, userId);
  console.log(‘User connected’, userId);
});

Once we receive event, which indicates that new user is connected, we make sure that the connected peer is not us. In this case, we establish connection with it by calling the “private” method _connectTo.

The callback for USER_DISCONNECTED is almost analogous so we won’t take a further look at it.

The next interesting part of the connect method is the snippet where we establish new Peer.js connection:

this.peer = new Peer(username, {
  host: location.hostname, port: 9000, path: ‘/chat’
});
this.peer.on(‘open’, function (userId) {
  self.setUsername(userId);
});
this.peer.on(‘connection’, function (conn) {
  self._registerPeer(conn.peer, conn);
  self.emit(Topics.USER_CONNECTED, conn.peer);
});

Once we invoke the constructor function Peer, provided by Peer.js, with the appropriate parameters, we bind to the open event. When the callback passed for the open event is being invoked, we receive the unique identifier of the current user, in the ideal case it will be the username entered in the home screen. Once we receive the user identifier we can save it.

When we receive connection event we register the connected peer and emit USER_CONNECTED event. The USER_CONNECTED event will be handled by the ChatBox, which will lead to change of the state of the UI.

The full content of ChatProxy could be found at GitHub.

app.jsx

The initial view of the user would be:

<section id=”container”>
  <div class=”reg-form-container”>
    <label for=”username-input”>Username</label>
    <input type=”text” id=”username-input” class=”form-control”>
    <br>
    <button id=”connect-btn” class=”btn btn-primary”>Connect</button>
  </div>
</section>

Once rendered in the browser, this would be a simple text box asking the client for optional username. In order to see what happens once the user click on the #connect-btn, let’s take a look at the app.jsx file, which is located at /public/app.jsx:

/** @jsx React.DOM */$(function () {
  $(‘#connect-btn’).click(function () {
    initChat($(‘#container’)[0],
      $(‘#username-input’).val());
  });  function initChat(container, username) {
    React.renderComponent(<ChatBox chatProxy={new ChatProxy()}
      username={username}></ChatBox>, container);
  }  window.onbeforeunload = function () {
    return ‘Are you sure you want to leave this page?’;
  };});

When the user clicks on #connect-btn we render the ChatBox component inside the #container element. So now let’s see what the ChatBox does:

ChatBox.jsx

At /public/src/components/chat/ create a file called ChatBox.jsx and add the following content:

/** @jsx React.DOM */’use strict’;var ChatBox = React.createClass({
  getInitialState: function () {
    return { users: [] };
  },  componentDidMount: function () {
    this.chatProxy = this.props.chatProxy;
    this.chatProxy.connect(this.props.username);
    this.chatProxy.onMessage(this.addMessage.bind(this));
    this.chatProxy.onUserConnected(this.userConnected.bind(this));
    this.chatProxy.onUserDisconnected(this.userDisconnected.bind(this));
  },  userConnected: function (user) {
    var users = this.state.users;
    users.push(user);
    this.setState({
      users: users
    });
  },  userDisconnected: function (user) {
    var users = this.state.users;
    users.splice(users.indexOf(user), 1);
    this.setState({
      users: users
    });
  },  messageHandler: function (message) {
    message = this.refs.messageInput.getDOMNode().value;
    this.addMessage({
      content: message,
      author : this.chatProxy.getUsername()
    });
    this.chatProxy.broadcast(message);
  },  addMessage: function (message) {
    if (message) {
      message.date = new Date();
      this.refs.messagesList.addMessage(message);
    }
  },  render: function () {
    return (
      <div className=”chat-box” ref=”root”>
        <div className=”chat-header ui-widget-header”>React p2p Chat</div>
        <div className=”chat-content-wrapper row”>
          <MessagesList ref=”messagesList”></MessagesList>
          <UsersList users={this.state.users} ref=”usersList”></UsersList>
        </div>
        <MessageInput
          ref=”messageInput”
          messageHandler={this.messageHandler}>
        </MessageInput>
      </div>
    );
  }
});

Let’s take a look at the render method:

render: function () {
  return (
    <div className=”chat-box” ref=”root”>
      <div className=”chat-header ui-widget-header”>React p2p Chat</div>
      <div className=”chat-content-wrapper row”>
        <MessagesList ref=”messagesList”></MessagesList>
        <UsersList users={this.state.users} ref=”usersList”></UsersList>
      </div>
      <MessageInput
        ref=”messageInput”
        messageHandler={this.messageHandler}>
      </MessageInput>
    </div>
  );
}

The render method returns the markup, which should be rendered. We use components, which are already defined and available in the given scope (components like MessagesList and MessageInput).

Once the component has been mounted the componentDidMount method is being invoked:

componentDidMount: function () {
  this.chatProxy = this.props.chatProxy;
  this.chatProxy.connect(this.props.username);
  this.chatProxy.onMessage(this.addMessage.bind(this));
  this.chatProxy.onUserConnected(this.userConnected.bind(this));
  this.chatProxy.onUserDisconnected(this.userDisconnected.bind(this));
},

In this method we create new ChatProxy, invoke its method connect and add event handlers. Once we receive a new message the callback registered for onMessage will be invoked, once a user is connected the callback userConnected will be invoked and once a peer is being disconnected the callback userDisconnected will be invoked. We use Function.prototype.bind in order to change the context for the callbacks with appropriate one.

userConnected and userDisconnected are similar:

userConnected: function (user) {
  var users = this.state.users;
  users.push(user);
  this.setState({
    users: users
  });
},userDisconnected: function (user) {
  var users = this.state.users;
  users.splice(users.indexOf(user), 1);
  this.setState({
    users: users
  });
}

They both change the state, which leads to call of the render method with the new state, which reflects on other components and respectively on the current UI.

In the addMessage method we have:

addMessage: function (message) {
  if (message) {
    message.date = new Date();
    this.refs.messagesList.addMessage(message);
  }
}

The interesting part here is the line: this.refs.messagesList.addMessage(message);, where we use this.refs. This is built-in React.js feature, which allows us to reference to existing child components. Once we set the ref attribute of given component (like <MessagesList ref=”messagesList”></MessagesList>) we can later access the component by using this.refs.REF_ATTRIBUTE_VALUE.

MessagesList.jsx

Inside /public/src/components/chat/ add file called MessagesList.jsx and add the following content:

/** @jsx React.DOM */’use strict’;var MessagesList = React.createClass({  getInitialState: function () {
    return { messages: [] };
  },  addMessage: function (message) {
    var messages = this.state.messages,
        container = this.refs.messageContainer.getDOMNode();
    messages.push(message);
    this.setState({ messages: messages });
    // Smart scrolling – when the user is
    // scrolled a little we don’t want to return him back
    if (container.scrollHeight –
        (container.scrollTop + container.offsetHeight) >= 50) {
      this.scrolled = true;
    } else {
      this.scrolled = false;
    }
  },  componentDidUpdate: function () {
    if (this.scrolled) {
      return;
    }
    var container = this.refs.messageContainer.getDOMNode();
    container.scrollTop = container.scrollHeight;
  },  render: function () {
    var messages;
    messages = this.state.messages.map(function (m) {
      return (
        <ChatMessage message={m}></ChatMessage>
      );
    });
    if (!messages.length) {
      messages = <div className=”chat-no-messages”>No messages</div>;
    }
    return (
      <div ref=”messageContainer” className=”chat-messages col-xs-9″>
        {messages}
      </div>
    );
  }
});

Let’s take a look at the render method of this component:

render: function () {
  var messages;
  messages = this.state.messages.map(function (m) {
    return (
      <ChatMessage message={m}></ChatMessage>
    );
  });
  if (!messages.length) {
    messages = <div className=”chat-no-messages”>No messages</div>;
  }
  return (
    <div ref=”messageContainer” className=”chat-messages col-xs-9″>
      {messages}
    </div>
  );
}

Initially we iterate over all messages from the state of the current component (this.state.messages). Using Array.prototype.map we turn our messages array into ChatMessages and later render them into the div.chat-messages.

In addMessage we add new chat messages by appending them to the list of all messages:

addMessage: function (message) {
  var messages = this.state.messages,
      container = this.refs.messageContainer.getDOMNode();
  messages.push(message);
  this.setState({ messages: messages });
  // Smart scrolling – when the user is
  // scrolled a little we don’t want to return him back
  if (container.scrollHeight –
      (container.scrollTop + container.offsetHeight) >= 50) {
    this.scrolled = true;
  } else {
    this.scrolled = false;
  }
}

The interesting part here is:

if (container.scrollHeight –
    (container.scrollTop + container.offsetHeight) >= 50) {
  this.scrolled = true;
} else {
  this.scrolled = false;
}

Basically, this snippet checks whether the user have scrolled more than 50pxs. If he did, we don’t want to scroll to bottom once he have started reading messages from the history of the chat. That’s why depending on whether the user have or haven’t scrolled we set this.scrolled to true or false.

We use this.scrolled in componentDidUpdate:

componentDidUpdate: function () {
  if (this.scrolled) {
    return;
  }
  var container = this.refs.messageContainer.getDOMNode();
  container.scrollTop = container.scrollHeight;
}

Once the component is going to be updated (for example because of new message added), we check whether the user have scrolled and if he had, we set scrollTop to the appropriate value. For getting the scroll container we use this.refs, as explained above.

MessageInput.jsx

This is the last component we will look at.

/** @jsx React.DOM */’use strict’;var MessageInput = React.createClass({  mixins: [React.addons.LinkedStateMixin],  keyHandler: function (event) {
    var msg = this.state.message.trim();
    if (event.keyCode === 13 && msg.length) {
      this.props.messageHandler(msg);
      this.setState({ message: ” });
    }
  },  getInitialState: function () {
    return { message: ” };
  },  render: function () {
    return (
      <input type=”text”
        className = “form-control”
        placeholder=”Enter a message…”
        valueLink={this.linkState(“message”)}
        onKeyUp={this.keyHandler}/>
    );
  }
});

In this component we use the mixin React.addons.LinkedStateMixin, which adds the method linkState to our component. Once the linkState method is called we can create two-way data binding between given input and property of our state. The name of the property depends on the value we pass to the linkState call. For example if we invoke this.linkState(‘value’), once the value of the input is being changed, this will reflect on this.state.value.

Another interesting moment here is the key handler. On key up of input.form-control the keyHandler method will be called. The method checks whether the event was called by pressing enter and whether the length of the trimmed value of the current message is more than zero, if it is, it updates the value of the current message to be the empty string and invokes this.props.messageHandler. this.props.messageHandler is passed by the ChatBox component as property of the MessageInput:

<MessageInput
  ref=”messageInput”
  messageHandler={this.messageHandler}>
</MessageInput>

Run the project…

The next step is to run the project by:

node index.js && open http://localhost:3001

I hope the blog post was fun and useful! 🙂

If you need help in any kind of development project & I can also provide you consultancy about your project. I am top rated freelancer. You can hire me directly on Upwork. You can also hire me on Freelancer.

If you have any comment, question, or recommendation, feel free to post them in the comment section below!

// Do this
Route::view(‘about’, ‘texts.about’);

Tip 16: Blade @auth

Instead of if-statement to check logged in user, use @auth directive

Typical way:

@if(auth()->user())
// The user is authenticated.
@endif

Shorter:

@auth
// The user is authenticated.
@endauth

Tip 17: Model all columns

When calling Eloquent’s Model::all() , you can specify which columns to return.

$users = User::all([‘id’, ‘name’, ’email’]);

Tip 18: To Fail or not to Fail

In addition to findOrFail() , there’s also Eloquent method firstOrFail() which will return 404 page if no records for query are found.

$user = User::where(’email’,’codechief@gmail.com’)->firstOrFail();

Tip 19: Use hasMany to create Many

If you have hasMany() relationship, you can use saveMany() to save multiple “child” entries from your “parent” object, all in one sentence.

$post = Post::find(1);$post->comments()->saveMany([ new Comment([‘message’ => ‘First comment’]),
 new Comment([‘message’ => ‘Second comment’]),]);

Tip 20: More convenient DD

Instead of doing dd($result); you can put ->dd() as a method directly at the end of your Eloquent sentence, or any Collection.

// Instead of
$users = User::where(‘name’, ‘Taylor’)->get();
dd($users);// Do this
$users = User::where(‘name’, ‘Taylor’)->get()->dd();

Tip 21: How to avoid error in {{ $post->user->name }} if user is deleted?

You can assign a default model in belongsTo relationship, to avoid fatal errors when calling it like {{ $post->user->name }} if $post->user doesn’t exist.

/**
* Get the author of the post.
*/
public function user()
{
   return $this->belongsTo(‘App\User’)->withDefault();
}

Tip 22: Route Model Binding: You can define a key

You can do Route model binding like Route::get(‘api/users/{user}’, function (App\User $user) { … } — but not only by ID field. If you want {user} to be a username
field, put this in the model:

public function getRouteKeyName() {   return ‘username’;}

Tip 23: Redirect to Specific Controller Method

You can redirect() not only to URL or specific route, but to a specific Controller’s specific method, and even pass the parameters. Use this:

return redirect()->action(‘SomeController@method’,[‘param’ => $value]);

Tip 24: Touch parent updated_at easily

If you are updating a record and want to update the updated_at column of parent relationship (like, you add new post comment and want posts.updated_at to renew), just use $touches = [‘post’]; property on child model.

class Comment extends Model
{
 /**
  * All of the relationships to be touched.
  *
  * @var array
  */ protected $touches = [‘post’];}

Tip 25: Use withCount() to Calculate Child Relationships Records

If you have hasMany() relationship, and you want to calculate “children” entries, don’t write a special query. For example, if you have posts and comments on your User model, write this withCount() :

public function index()
{
  $users = User::withCount([‘posts’, ‘comments’])->get();
  return view(‘users’, compact(‘users’));
}

And then, in your Blade file, you will access those number with [ relationship ]_count properties

@foreach ($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td class=”text-center”>{{ $user->posts_count }}</td>
<td class=”text-center”>{{ $user->comments_count }}</td>
</tr>
@endforeach

Tip 26: Blade Directives: IncludeIf, IncludeWhen, IncludeFirst

If you are not sure whether your Blade partial file actually would exist, you may use these condition commands.

This will load header only if Blade file exists

@includeIf(‘partials.header’)

This will load header only for user with role_id 1

@includeWhen(auth()->user()->role_id == 1, ‘partials.header’)

This will try to load adminlte.header, if missing — will load default.header

@includeFirst(‘adminlte.header’, ‘default.header’)

Hope this Laravel tips and tricks tutorial will be helpful for you.

If you need help in any kind of development project & I can also provide you consultancy about your project. I am top rated freelancer. You can hire me directly on Upwork. You can also hire me on Freelancer.

If you have any comment, question, or recommendation, feel free to post them in the comment section below!

0 0 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

Let’s Socialize

Popular Post

Share

Share on facebook
Facebook
Share on twitter
Twitter
Share on linkedin
LinkedIn
Share on tumblr
Tumblr
Share on skype
Skype
Share on whatsapp
WhatsApp

Flutter vs. React Native

Flutter: Flutter is a UI toolkit used for crafting beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. Flutter is not

Read More »
0
Would love your thoughts, please comment.x
()
x