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 Room
component:
// 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 calledcoding 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 updateRoom
'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.