UPDATE: This tutorial is no longer up to date (I haven’t looked into React for a while) - it’s just for reference.

Welcome to the first part of React.js tutorial where we will build application that allows user to communicate in real-time. In this episode we build our first component - text-chat.

Prequsition

In this tutorial I will use bower to install all dependencies, browserify for managing JavaScript modules, Bootstrap for basic styling and python’s SimpleHTTPServer module (python -m SimpleHTTPServer) to serve the app locally. I also have added path to local npm packages to my search path: export PATH="./node_modules/.bin:$PATH" and I also have .bowerrc file to download all libraries to ‘vendor’ folder (instead of ‘componens’):

Here is our .bowerrc file:

{
  "directory": "vendor"
}

Motivation

Recently I’ve popped into React.js tutorial on Arkency blog. The library seemed powerful and simple at the same time so I decided to give it a shot.

The last application I build for my client is using OpenTok to support video and text-text communication. Initial version of the app was build using plain jQuery. I wanted to check if re-writing front-end part to use React.js can improve the code.

What does React.js do?

Before we move into the code, let’s look at the short description of React.js:

React - a JavaScript library for building user interfaces.

Lots of people use React as the V in MVC. Since React makes no assumptions about the rest of your technology stack, it’s easy to try it out on a small feature in an existing project.

React uses a virtual DOM diff implementation for ultra-high performance. It can also render on the server using Node.js — no heavy browser DOM required.

React implements one-way reactive data flow which reduces boilerplate and is easier to reason about than traditional data binding.

React.js is just a library for building view layer it can be used with other JavaScript framework (like Backbone) easly. Because it’s using VirtualDOM it’s blazingly fast.

Setting up the app

Ok, so let’s start this tutorial off by defining our package.json file:

{
  "name": "rt-session",
  "devDependencies": {
    "bower": "1.3.x",
    "browserify": "5.9.x",
    "watchify": "1.0.x",
    "uglifyify": "1.x"
  },
  "engines": {
    "node": ">=0.10.0"
  },
  "scripts": {
    "watch": "watchify -e app/bootstrap.js -o public/js/bundle.js -v",
    "build": "browserify -e app/bootstrap.js -t uglifyify -o public/js/bundle.min.js"
  }
}

To install all node dependiencies execute npm install. After it finishes let’s move to bower manifest file to define all required JavaScript libraries for our application:

Now update our bower.json:

{
  "name": "rt-session",
  "dependencies": {
    "bootstrap": "~3.2.0",
    "eventEmitter": "~4.2.7",
    "react": "~0.11.1"
  }
}

To install them execute bower install. After it finishes we are ready to write our application.

Index page

Let’s create basic markup for our application:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Real-time app with React.js</title>
  <link rel="stylesheet" href="/vendor/bootstrap/dist/css/bootstrap.min.css">
  <link rel="stylesheet" href="/public/css/app.css">
</head>
<body>
  <div class="container">
    <nav class="navbar navbar-default" role="navigation">
      <div class="navbar-header">
      <a class="navbar-brand" href="#">RT Session</a>
      </div>
    </nav>
    <div class="row">
      <div class="col-md-3">
        <div id="chat"></div>
      </div>
      <div class="col-md-9">
        <!-- video -->
      </div>
    </div>
  </div>
  <script src="/public/js/bundle.js"></script>
</body>
</html>

We need to add some CSS for our chat component:

// public/css/app.css

.chat {
  position: fixed;
  bottom: 0;
  width: 290px;
  height: 325px;
  padding: 10px;
  border: 1px solid #e7e7e7;
  background: #f8f8f8;
  border-radius: 5px;
}

.messages {
  height: 250px;
  overflow-y: auto;
}

Implementing chat component

Our chat components is a composition of 2 sub-components:

  • MessageList - to display all messages from participants. Basically it’s a list of Message components
  • InputField - to create a new message that will be send to everybody

We start our implementation from bottom to top (message -> message list -> chat) so let’s define Message first:

// app/components/message.js

var React = require('../../vendor/react/react');

module.exports = React.createClass({
  render: function() {
    return React.DOM.li({
      className: 'list-group-item'
    }, this.props.message);
  }
});

It’s very simple React component which do nothing more than displaying a text in the li tag.

The component is used by MessageList which iterates over messages that are passed as argument:

// app/components/message_list.js

var React = require('../../vendor/react/react');
var Message = require('./message');

module.exports = React.createClass({
  componentDidUpdate: function() {
    // Scroll message list to the bottom
    this.getDOMNode().scrollTop = this.getDOMNode().scrollHeight;
  },
  render: function() {
    var messages = this.props.messages.map(function(message, i) {
      return Message({
        key: i,
        message: message
      });
    });
    return React.DOM.ul({
      className: 'list-group messages'
    }, messages);
  }
});

It also make sure to scroll down the chat on new message.

Let’s define our chat component:

// app/components/chat.js

var React = require('../../vendor/react/react');
var MessageList = require('./message_list');

module.exports = React.createClass({
  getInitialState: function() {
    return {
      messages: [],
      newMessage: ''
    };
  },
  send: function(e) {
    if (e.keyCode !== 13 || e.target.value === "") {
      return;
    }

    // Append new message
    var newMessage = this.props.name + ": " + e.target.value;
    this.addMessage(newMessage);
  },
  addMessage: function(newMessage) {
    this.state.messages.push(newMessage);

    // Clear the input
    this.refs.messageForm.getDOMNode().value = '';

    return this.setState({
      messages: this.state.messages
    });
  },
  render: function() {
    return React.DOM.div({
      className: 'chat'
    }, this.messageList(), this.messageForm());
  },
  messageForm: function() {
    return React.DOM.input({
      className: 'form-control',
      name: 'message',
      onKeyUp: this.send,
      placeholder: 'Send message...',
      ref: 'messageForm',
      type: 'text'
    });
  },
  messageList: function() {
    return MessageList({
      messages: this.state.messages
    });
  }
});

To get user input we could also create separate MessageForm component that would trigger events on ‘Enter’ ket but for our tutorial built-in Input component should be enough.

After all necessary components have been defined we can create a bootstrap script that will kick off our application:

// app/bootstrap.js

var React = require('../vendor/react/react');
var Chat = require('./components/chat');

window.app = (function() {
  var name = prompt("Please enter your name");
  React.renderComponent(Chat({
    name: name
  }), document.getElementById('chat'));
})();

Now we should be able to preview our app in the browser with ability to create new messages.

Connect with OpenTok

To start using OpenTok we need to register a new account on TokBox website and create project for our app. After that we should receive our api key.

For purpose of this tutorial we will generate session id and token via TokBox admin panel. Credentials will be shared between all participants. Basically it means that all users will have the same access rights in our session.

On the real production app we should generate separate token for each user depending on their role in the system. If we want some kind of “room” system where users can create their own chats, we should also generate separate session ids.

Let’s go back to our code and create our Session object:

// app/libs/session.js

var EventEmitter = require ('../../vendor/eventEmitter/EventEmitter');

var Session = function(apiKey, sessionId) {
  this.apiKey = apiKey;
  this.sessionId = sessionId;
};

Session.prototype = Object.create(EventEmitter.prototype);
Session.prototype.init = function(token) {
  var self = this;
  this.session = OT.initSession(this.apiKey, this.sessionId);
  this.session.on('signal', function(event) {
    if(event.type === 'signal:message') {
      self.emitEvent('message', [event.data]);
    }
  });
  this.session.connect(token, function(event) {
    self.emitEvent('connected');
  });
};
Session.prototype.message = function(data) {
  this.session.signal({
    type: 'message',
    data: data
  });
};

module.exports = Session;

Whenever session successfuly connect to OpenTok it will emit ‘connected’ event. Also when OpenTok session receive signal with ‘message’ type, our session object will emit ‘message’ event. We will subscribe to this event in the Chat component. Here is our updated version of Chat component:

// app/components/chat.js

var React = require('../../vendor/react/react');
var MessageList = require('./message_list');

module.exports = React.createClass({
  getInitialState: function() {
    return {
      messages: [],
      newMessage: ''
    };
  },
  componentDidMount: function() {
    this.props.session.addListener('message', this.receiveMessage);
  },
  componentWillUnmount: function() {
    this.props.session.removeListener('message', this.receiveMessage);
  },
  send: function(e) {
    if (e.keyCode !== 13 || e.target.value === "") {
      return;
    }

    // Send new message
    this.props.session.message({
      name: this.props.name,
      message: e.target.value
    });
    this.refs.messageForm.getDOMNode().value = '';
  },
  addMessage: function(newMessage) {
    this.state.messages.push(newMessage);

    return this.setState({
      messages: this.state.messages
    });
  },
  receiveMessage: function(data) {
    this.addMessage(data.name + ": " + data.message);
  },
  render: function() {
    return React.DOM.div({
      className: 'chat'
    }, this.messageList(), this.messageForm());
  },
  messageForm: function() {
    return React.DOM.input({
      className: 'form-control',
      name: 'message',
      onKeyUp: this.send,
      placeholder: 'Send message...',
      ref: 'messageForm',
      type: 'text'
    });
  },
  messageList: function() {
    return MessageList({
      messages: this.state.messages
    });
  }
});

When the component is rendered on the page for the first time we subscribe to the ‘message’ event (via componentWillMount callback) and we unsubscribe (via componentWillUnmount callback) from that event whenever the component is going to be destroyed.

The end

This finishes our first part of the tutorial. In the next part we will add video-chat.

Application code can be found on GitHub.

If you have any problems or questions you can ask me on Twitter @lowski_ or in the comments.