Real-Time React with Socket.io: Building a Pair Programming App

The best things are real-time things, so I won't bore you with another introduction on why we all need to build real-time features into our applications. People want to chat and share files and collaborate on documents and projects and put pictures of cats on things in real-time.

So, how is a humble React developer to keep up with this incessant demand for real-time functionality?

Well, in turns out that React, Express and Socket.io play really nice together, once you get past of few "got cha"-type hiccups.

In order to explore these technologies more fully, I built out a fun pair programming app that allows users to choose a code challenge (courtesy of Project Euler) and enter into a chatroom-like page to collaborate on programming solutions in real-time with other participants.

Users can collaboratively write code into a shared text editor to solve the challenge (with the help of Code Mirror), toggle the language being used, toggle the text editor's theme and download their solution to a file when they're done.

You can check out the deployed version of the app here, or view the final code here.

In this post, we'll take a look at how to set up React, Express and Socket.io to use room-specific subscriptions to allow users to collaborate on code in real-time.

Let's get started!

Application Architecture

Before we start writing code, let's do a quick overview of our app. Users will arrive on the home page, where they are assigned a random user name (which they can edit later). Then, they can click a link to visit the room of a given code challenge.

The code challenge room will display the code challenge title and description, the list of users present in the room, and a collaborative text editor area (with the help of Code Mirror).

The user will also see a button to "save" their solution, which will download a file with the code the user(s) wrote in that text area.

In this post, we'll set up our basic application architecture, and then focus on the real-time coding feature.

Components and Routes

Our component tree is fairly simple. We'll have one top-level container, App, that dynamically renders its children with the help of React Router. There will be two direct children:

  • The HomePage component, which will contain the presentational component that renders the list of code challenges.
  • The Room component, which will display the selected code challenge, list of participating users and collaborative code editor.

We do want our routes to change when a user clicks on the link to a specific challenge and enters the room for that challenge.

Let's set up our router.

Routes and Router

// src/routes.js
import React from 'react';  
import { Route, IndexRoute } from 'react-router';  
import App from './components/App';  
import HomePage from './components/HomePage';  
import Room from './components/Room'

export default (  
  <Route path="/" component={App}>
    <IndexRoute component={HomePage} />
    <Route path="/rooms/:id" component={Room} />
  </Route>
);

We'll render these routes and their associated component tree via the entry point of our app, index.js:

// src/index.js

/* eslint-disable import/default */

import 'babel-polyfill' ;  
import React from 'react';  
import { render } from 'react-dom';  
import { Router, browserHistory } from 'react-router';  
import routes from './routes';  
import { Provider } from 'react-redux';  
import configureStore from './store/configureStore';

const store = configureStore()

render(  
  <Provider store={store}>
    <Router routes={routes} history={browserHistory} />
  </Provider>,
 document.getElementById('main')
);

Components

We'll set up our App component to render this.props.children, which will be dynamically populated by either the HomePage or the Room component, depending on the selected route.

import React from 'react';  
import Header from './common/Header';

export default class App extends React.Component {  
  render() { 
    return (
      <div>
        <Header />
        <div className="container">
          {this.props.children}
        </div>
      </div>
    )
  }
}

Our HomePage component renders a presentational component that lists our available code challenges.

import React from 'react'  
import {connect} from 'react-redux';  
import {bindActionCreators} from 'redux';  
import * as challengesActions from '../actions/challengesActions';  
import ChallengesList from './ChallengesList';

class HomePage extends React.Component {  
  componentDidMount() {
    if (this.props.challenges.length == 0) {
      this.props.actions.getChallenges()
    }
  }

  render() {
    return (
      <div>
        <ChallengesList 
          challenges={this.props.challenges} />
      </div>
    )
  }
}

function mapStateToProps(state) {  
  return {challenges: state.challenges}
}

function mapDispatchToProps(dispatch) {  
  return {actions: bindActionCreators(challengesActions, dispatch)}
}

export default connect(mapStateToProps, mapDispatchToProps)(HomePage);  

The HomePage component dispatches an action getChallenges, that fetches the collection of code challenges from our (separate and not discussed here) API. Then, it uses mapStateToProps to grab the challenges from state and pass them to the component as part of props.

Note: We won't be discussing the action creator functions or reducers here. Check out these files in the repo to learn more.

HomePage's child component, ChallengesList, iterates over and displays those challenges as links to each challenge's page.

import React from 'react'  
import {Link} from 'react-router'  
import {ListGroup, ListGroupItem} from 'react-bootstrap'

const ChallengesList = (props) => {  
  return (
    <ListGroup>
      {props.challenges.map((challenge, i) => {
        return ( 
           <ListGroupItem key={i}>
             <Link to={`/rooms/${challenge.id}`}>
               {challenge.title}
             </Link>
           </ListGroupItem>
          )
      })}
    </ListGroup>
  )
}

export default ChallengesList;  

Here, we use the Link component imported from React Router so that when a user clicks on the link to rooms/1, for example, our app will render the component that we mapped to the rooms/:id route, which is Room.

The Room component is where the magic happens, and we'll jump into that now.

Code Mirror + React

The Room component has two jobs to do before we can start worrying about bringing in our socket connections.

First things first: get the correct challenge and display it. We'll use mapStateToProps to identify the challenge to display, grab it from state and pass it to our component as part of props.

// src/components/Room.js
import React from 'react';  
import * as actions from '../actions/challengesActions'  
import {connect} from 'react-redux';  
import {bindActionCreators} from 'redux';

class Room extends React.Component {  
  componentDidMount() {
    if (this.props.challenge.id == undefined) {
      this.props.actions.getChallenges();
    }
  }

  render() {
    return (
      <div>
        <h1>{this.props.challenge.title}</h1>
        <p>{this.props.challenge.description}</p>
        // coming soon: code mirror text editor!
      </div>
    )
  }
}

function mapStateToProps(state, ownProps) {  
  if (state.challenges.length > 0) {
    const challenge = state.challenges.filter(challenge => 
      {return challenge.id == ownProps.params.id})[0]
    return {challenge: challenge}
  } else {
    return {challenge: {title: '', description: ''}}
  }
}

function mapDispatchToProps(dispatch) {  
  return {actions: bindActionCreators(actions, dispatch)}
}

export default connect(mapStateToProps, mapDispatchToProps)(Room)  

Now that our Room component knows how to display the correct challenge, we're ready to bring in Code Mirror to display a text-editor-esque text area for our users to write their code in.

Setting Up Code Mirror

Code Mirror is awesome. More specifically,

CodeMirror is a versatile text editor implemented in JavaScript for the browser. It is specialized for editing code, and comes with a number of language modes and addons that implement more advanced editing functionality.*

Not surprisingly, there are a few different ways to bring Code Mirror into your React app. I chose Jed Watson's react-codemirror package.

It wasn't too difficult to set up.

First:

  • npm install --save react-codemirror

Then, in our Roomcomponent:

// src/components/Room.js
...
import Codemirror from 'react-codemirror';  
import 'codemirror/lib/codemirror.css';  
import 'codemirror/theme/monokai.css';  
import 'codemirror/mode/javascript/javascript.js'

...

render() {  
  const options = {
     lineNumbers: true,
     mode: 'javascript',
     theme: 'monokai'
  }
  return (
    <div>
      <h1>{this.props.challenge.title}</h1>
      <p>{this.props.challenge.description}</p>
      <Codemirror 
        value={"hello world!"} 
        onChange={coming soon!} 
        options={options} />
    </div>
   )
}

Our Codemirror component is configurable with the options object. We can give Codemirror the language designation under options.mode and the theme designation under options.theme. We simply need to import the desired language and theme from the code mirror library, as shown above.

Codemirror renders the text given to it under the prop of value and responds to an event, onChange.

In order to dynamically render text as the user types, we need to tell Codemirror how and when to change its value prop.

So, we need our Room component to be able to keep track of whatever gets passed to Codemirror as value and to be able to change whatever gets passed to Codemirror as value in response to the user's typing action.

If you're thinking that it sounds like Room needs to keep track of the "code" text as part of its own internal state, you're right!

Room State + Updating Codemirror

We'll give Room an internal state with a property of code. We'll pass this.state.code into Codemirror under the prop of value and update this.state.code via the onChange event that Codemirror responds to.

// src/components/Room.js
...
import Codemirror from 'react-codemirror';  
import 'codemirror/lib/codemirror.css';  
import 'codemirror/theme/monokai.css';  
import 'codemirror/mode/javascript/javascript.js'

...

class Room extends React.Component {  
  constructor(props) {
    super(props)
    this.state = {code: ''}
  }
  ...

  updateCodeInState(newText) {
    this.setState({code: newText})
  }

  render() {
    const options = {
       lineNumbers: true,
       mode: 'javascript',
       theme: 'monokai'
    }
    return (
      <div>
        <h1>{this.props.challenge.title}</h1>
        <p>{this.props.challenge.description}</p>
        <Codemirror 
          value={this.state.code} 
          onChange={this.updateCodeInState.bind(this)} 
          options={options} />
      </div>
    )
  }
}

We set our Room component's state in the constructor function and defined a function, updateCodeInState to use as the onChange callback function for our Codemirror component.

Codemirror will call updateCodeInState whenever there is a change to the code mirror text area, passing the function an argument of the text contained in that text area.

updateCodeInState creates a new copy of Room's state, causing our component to re-render, passing that new value of this.state.code into the Codemirror component under the prop of value.

Now that we have that working from the point of view of our single user, let's integrate Socket.io to enable all clients viewing the page to see and generate new text for our code mirror text editor, in real-time.

Express + Socket.io

Socket.io is a full-stack WebSockets implementation in JavaScript. It has both server-side and client-side offerings to enable us to initiate and maintain a socket connection and layer multiple "channels" over that connection.

We'll set up a socket connection running on our express server and teach that connection how to respond to certain events emitted by our client.

Right now, our server is pretty straightforward:

// tools/server.js
import express from 'express';  
import webpack from 'webpack';  
import path from 'path';  
import config from '../webpack.config.dev';  
import open from 'open';  

/* eslint-disable no-console */

const port = 3000;  
const app = express();  
const compiler = webpack(config);

app.use(require('webpack-dev-middleware')(compiler, {  
  noInfo: true,
  publicPath: config.output.publicPath
}));

app.use(require('webpack-hot-middleware')(compiler));  

app.get('*', function(req, res) {  
  res.sendFile(path.join( __dirname, '../src/index.html'));
});

const server = app.listen(port, function(err) {  
  if (err) {
    console.log(err);
  } else {
    open(`http://localhost:${port}`);
  }
});

We'll install the Socket.io NPM package and require it in our server file.

First, run npm install --save socket.io

Then, set up your socket connection like this:

...
const server = app.listen(port, function(err) {  
  if (err) {
    console.log(err);
  } else {
    open(`http://localhost:${port}`);
  }
});

const io = require('socket.io')(server);  

We'll give our socket connection some super basic instructions on what do to when a connection has been successfully made from the client:

io.on('connection', (socket) => {  
  console.log('a user connected');

  socket.on('disconnect', () => {
    console.log('user disconnected');
  });
}

We'll come back to our server in a minute. First, let's teach our client how to initiate the socket connection.

Setting Up Socket.io Client-Side

To set up Socket.io on the client, we'll pull in the Socket.io client-side library. We'll do this in the Room component, because that it the only component that needs to communicate over sockets.

The following two lines:

const io = require('socket.io-client')  
const socket = io()  

included at the top of src/components/Room.js will initiate the request to open a socket connection with our Express server.

Now that our connection is up and running, we need to teach that connection how to emit events to and receive events from the correct clients.

In other words, if I am looking at the Room component rendering challenge #1, I should not see the code that something is typing into the text editor via their browser viewing challenge #2.

In order to take care of this, we'll teach our Room component, when it mounts, to "subscribe to" or "join" a specific socket channel, and we'll always emit events via the channel associated to our current component.

Socket.io Room Subscriptions

Joining rooms

When should a user "join a room"? It is when the Room component mounts, that we should consider the user to be joining the room.

So, we'll use the componentDidMount lifecycle method on our Room component to send a message to our socket connection that a new client is subscribing to the channel associated with this particular room.

// src/components/Room.js

... 
componentDidMount() {  
 if (this.props.challenge.id == undefined) {
    this.props.actions.getChallenges();
  } else {
    socket.emit('room', {room: this.props.challenge.id});
    this.setState({users: users})
  }
}
...

There's only one problem with this. What if the user arrives at this page and our if condition evaluates to true? In other words, what happens if we didn't have any challenges in state, and had to use our componentDidMount method to fetch them?

If this happens, it will dispatch the getChallenges action creator function, which will make our API call, retrieve the challenges and pass them to the reducer. The reducer will tell the store to create a new copy of state containing the newly requested code challenges.

This will cause our component to re-render, triggering mapStateToProps to run again, this time finding the appropriate challenge in state and passing it to the Room component under this.props.challenge.

In this case, we would never hit our socket.emit code, because componentDidMount only ever runs once.

To catch this edge case, we'll take advantage of the componentWillReceiveProps lifecycle method, and place our socket.emit code there as well.

componentWillReceiveProps will execute at the end of the cycle of action dispatch -> reducer -> re-render described above.

// src/components/Room.js

... 
componentDidMount() {  
 if (this.props.challenge.id == undefined) {
    this.props.actions.getChallenges();
  } else {
    socket.emit('room', {room: this.props.challenge.id});
  }
}

componentWillReceiveProps(nextProps) {  
  socket.emit('room', {room: nextProps.challenge.id})
}

So, when a user loads the Room component by arriving on the page to view a specific challenge, our component will emit an event via our socket connection. We've designated this event as a 'room' event, via the first argument given to socket.emit.

Now we need to teach our server how to respond to that event.

The server will respond by "joining" the room, which establishes a room-specific channel layered over our socket connection.

// tools/server.js
...

io.on('connection', (socket) => {  
  console.log('a user connected');

  socket.on('disconnect', () => {
    console.log('user disconnected');
  });

  socket.on('room', function(data) {
    socket.join(data.room);
  });
}
Leaving Rooms

A user leaves a room when the leave the page. This moment is marked by the React component lifecycle method, componentWillUnMount. It is here that we will emit the "leaving" event.

// src/components/Room.js

...

componentWillUnmount() {  
    socket.emit('leave room', {
      room: this.props.challenge.id
    })
  }

...

Our server will respond by ending the given client's subscription to that room's channel:

// tools/server.js

...

io.on('connection', (socket) =>  {  
  ...
  socket.on('leave room', (data) => {
    socket.leave(data.room)
  })
})

Now that our room-specific channel is established, we're finally ready to emit "coding" events. Whenever a user types into the code mirror text area, we'll not only update that user's Room component's internal state to reflect the new text to be displayed, we'll send that text over our socket channel to all other subscribing clients.

Using Socket.io to Broadcast Real-Time Events

We want to broadcast new text as the user types it into the code mirror text area.

Lucky for us, we already have a callback function that fires at exactly that moment in time: updateCodeInState.

We'll add a line to that function to emit an event, sending a payload with the new text to the server via our socket channel.

// src/components/Room.js

...
updateCodeInState(newText) {  
  this.setState({code: newText})
  socket.emit('coding event', {
    room: this.props.challenge.id,
    newCode: newText
  })   
}

We'll teach our server to respond to this event by emitting the new code text to all subscribing clients:

// tools/server.js
...

io.on('connection', (socket) => {  
  console.log('a user connected');

  socket.on('disconnect', () => {
    console.log('user disconnected');
  });

  socket.on('room', function(data) {
    socket.join(data.room);
  });

  socket.on('coding event', function(data) {
    socket.broadcast.to(data.room).emit('receive code',   
      data)
  }
}

Now, we have to teach all subscribing clients how to receive the 'receive code' event, and respond to it by updating Room's internal state with the new code text.

We'll do that in the constructor function of our Room component, essentially telling our component to listen for an event, 'receive code', via the persistent socket connection.

// src/components/Room.js
...

class Room extends React.Component {  
  constructor(props) {
    super(props)
    this.state = {code: ''}
    socket.on('receive code', (payload) => {   
      this.updateCodeFromSockets(payload));
    }
  }

  updateCodeFromSockets(payload) {
    this.setState({code: payload.newCode})
  }
}

Let's break this down step by step:

  • A user types into the code mirror text area
  • The code mirror text area's onChange function fires.
  • This function updates the Room component's state for the user who is typing and emits an event called coding event over the socket channel for everyone else.
  • The socket connection receives that event server-side and uses the information contained in the event's payload to broadcast another event to the correct room.
  • All clients subscribing to that room's channel will receive this second event, called receive code.
  • These clients respond by firing a function, updateCodeFromSockets, that uses the information contained in the event's payload to update Room's internal state.

It's important to understand that the client that emitted the initial coding event event will not receive the secondary receive code event. Socket.io makes sure to avoid this for you.

And that's it!

Conclusion

This is just one example of how to implement the very flexible Socket.io. There's so much you can do with Socket.io and I was really pleased to see how nicely it integrates with React.

You can dig deeper into my implementation by cloning down this repo, and you can play around with the app here.

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus