In this blog post 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.
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.
Before we continue with the tutorial, lets 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 verse. 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.
Now lets get starting with the actual implementation of our WebRTC based chat.
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.
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.
Lets 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.
ChatBox component uses
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.
In this section we are going to setup our project…
Create a directory called
react-p2p and enter it:
package.json file with the content:
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
Now lets take a look at
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.
.bowerrc we define that we want all bower dependencies to be saved at
Now, in order to resolve all dependencies, run:
Now lets start with our implementation.
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:
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.
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.
Now lets 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.
/public/src/models/ create a file called
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. Lets take a look at it:
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_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:
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
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
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.
The initial view of the user would be:
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, lets take a look at the
app.jsx file, which is located at
When the user clicks on
#connect-btn we render the
ChatBox component inside the
#container element. So now lets see what the
/public/src/components/chat/ create a file called
ChatBox.jsx and add the following content:
Lets take a look at 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
Once the component has been mounted the
componentDidMount method is being invoked:
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.
userDisconnected are similar:
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.
addMessage method we have:
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
/public/src/components/chat/ add file called
MessagesList.jsx and add the following content:
Lets take a look at the
render method of this component:
Initially we iterate over all messages from the state of the current component (
Array.prototype.map we turn our messages array into
ChatMessages and later render them into the
addMessage we add new chat messages by appending them to the list of all messages:
The interesting part here is:
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. Thats why depending on whether the user have or haven’t scrolled we set
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.
This is the last component we will look at.
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
Another interesting moment here is the key handler. On key up of
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 is passed by the
ChatBox component as property of the
Run the project…
The next step is to run the project by:
I hope the blog post was fun and useful! :-)