Building a real-time Twitter stream with Node.js, React.js and Redis
Published by Matthew Daly at 28th September 2015 7:00 pm
In the last year or so, React.js has taken the world of web development by storm. A major reason for this is that it makes it possible to build isomorphic web applications - web apps where the same code can run on the client and the server. Using React.js, you can create a template that will be executed on the server when the page first loads, and then the same template can be used to re-render the content when it's updated, whether that's via AJAX, WebSockets or another method entirely.
In this tutorial, I'll show you how to build a simple Twitter streaming app using Node.js. I'm actually not the only person to have built this to demonstrate React.js, but this is my own particular take on this idea, since it's such an obvious use case for React.
What is React.js?
A lot of people get rather confused over this issue. It's not correct to compare React.js with frameworks like Angular.js or Backbone.js. It's often described as being just the V in MVC - it represents only the view layer. If you're familiar with Backbone.js, I think it's reasonable to compare it to Backbone's views, albeit with it's own templating syntax. It does not provide the following functionality like Angular and Backbone do:
- Support for models
- Any kind of helpers for AJAX requests
- Routing
If you want any of this functionality, you need to look elsewhere. There are other libraries around that offer this kind of functionality, so if you want to use React as part of some kind of MVC structure, you can do so - they're just not a part of the library itself.
React.js uses a so-called "virtual DOM" - rather than re-rendering the view from scratch when the state changes, it instead retains a virtual representation of the DOM in memory, updates that, then figures out what changes are required to update the existing DOM and applies them. This means it only needs to change what actually changes, making it faster than other client-side templating systems. Combined with the ability to render on the server side, React allows you to build high-performance apps that combine the initial speed and SEO advantages of conventional web apps with the responsiveness of single-page web apps.
To create components with React, it's common to use an XML-like syntax called JSX. It's not mandatory, but I highly recommend you do so as it's much more intuitive than creating elements with Javascript.
Getting started
You'll need a Twitter account, and you'll need to create a new Twitter app and obtain the security credentials to let you access the Twitter Streaming API. You'll also need to have Node.js installed (ideally using nvm
) - at this time, however, you can't use Node 4.0 because of issues with Redis. You will also need to install Redis and hiredis - if you've worked through my previous Redis tutorials you'll have these already.
We'll be using Gulp.js as our build system, and Bower to install some client-side packages, so they need to be installed globally:
$ npm install -g gulp bower
We'll also be using Compass to help with our stylesheets:
$ sudo gem install compass
With that all done, let's start work on our app. First, run the following command to create your package.json
:
$ npm init
I'm assuming you're well-acquainted enough with Node.js to know what this does, and can answer the questions without difficulty. I won't cover writing tests in this tutorial as, but set your test command to gulp test
and you should be fine.
Next, we need to install our dependencies:
1$ npm install --save babel compression express hbs hiredis lodash morgan react redis socket.io socket.io-client twitter2$ npm install --save-dev browserify chai gulp gulp-compass gulp-coveralls gulp-istanbul gulp-jshint gulp-mocha gulp-uglify jshint-stylish reactify request vinyl-buffer vinyl-source-stream
Planning our app
Now, it's worth taking a few minutes to plan the architecture of our app. We want to have the app listen to the Twitter Streaming API and filter for messages with any arbitrary string in them - in this case we'll be searching for "javascript", but you can set it to anything you like. That means that that part needs to be listening all the time, not just when someone is using the app. Also, it doesn't fit neatly into the usual request-response cycle - if several people visit the site at once, we could end up with multiple connections to fetch the same data, which is really not efficient, and could cause problems with duplicate tweets showing up.
Instead, we'll have a separate worker.js
file which runs constantly. This will listen for any matching messages on Twitter. When one appears, rather than returning it itself, it will publish it to a Redis channel, as well as persisting it. Then, the web app, which will be the index.js
file, will be subscribed to the same channel, and will receive the tweet and push it to all current users using Socket.io.
This is a good example of a message queue, and it's a common pattern. It allows you to create dedicated sections of your app for different tasks, and means that they will generally be more robust. In this case, if the worker goes down, users will still be able to see some tweets, and if the server goes down, the tweets will still be persisted to Redis. In theory, this would also allow you to scale your app more easily by allowing movement of different tasks to different servers, and several app servers could interface with a single worker process. The only downside I can think of is that on a platform like Heroku you'd need to have a separate dyno for the worker process - however, with Heroku's pricing model changing recently, since this needs to be listening all the time it won't be suitable for the free tier anyway.
First let's create our gulpfile.js
:
1var gulp = require('gulp');2var jshint = require('gulp-jshint');3var source = require('vinyl-source-stream');4var buffer = require('vinyl-buffer');5var browserify = require('browserify');6var reactify = require('reactify');7var mocha = require('gulp-mocha');8var istanbul = require('gulp-istanbul');9var coveralls = require('gulp-coveralls');10var compass = require('gulp-compass');11var uglify = require('gulp-uglify');1213var paths = {14 scripts: ['components/*.jsx'],15 styles: ['src/sass/*.scss']16};17gulp.task('lint', function () {18 return gulp.src([19 'index.js',20 'components/*.js'21 ])22 .pipe(jshint())23 .pipe(jshint.reporter('jshint-stylish'));24});2526gulp.task('compass', function() {27 gulp.src('src/sass/*.scss')28 .pipe(compass({29 css: 'static/css',30 sass: 'src/sass'31 }))32 .pipe(gulp.dest('static/css'));33});;3435gulp.task('test', function () {36 gulp.src('index.js')37 .pipe(istanbul())38 .pipe(istanbul.hookRequire())39 .on('finish', function () {40 gulp.src('test/test.js', {read: false})41 .pipe(mocha({ reporter: 'spec' }))42 .pipe(istanbul.writeReports({43 reporters: [44 'lcovonly',45 'cobertura',46 'html'47 ]48 }))49 .pipe(istanbul.enforceThresholds({ thresholds: { global: 90 } }))50 .once('error', function () {51 process.exit(0);52 })53 .once('end', function () {54 process.exit(0);55 });56 });57});5859gulp.task('coveralls', function () {60 gulp.src('coverage/lcov.info')61 .pipe(coveralls());62});6364gulp.task('react', function () {65 return browserify({ entries: ['components/index.jsx'], debug: true })66 .transform(reactify)67 .bundle()68 .pipe(source('bundle.js'))69 .pipe(buffer())70 .pipe(uglify())71 .pipe(gulp.dest('static/jsx/'));72});7374gulp.task('default', function () {75 gulp.watch(paths.scripts, ['react']);76 gulp.watch(paths.styles, ['compass']);77});
I've added tasks for the tests and JSHint if you choose to implement them, but the only ones I've actually used are the compass
and react
tasks. The compass
task compiles our Sass files into CSS, while the react
task uses Browserify to take our React components and various modules installed using NPM and build them for use in the browser, as well as minifying them. Note that we installed React and lodash with NPM? We're going to be able to use them in the browser and on the server, thanks to Browserify.
Next, let's create our worker.js
file:
1/*jslint node: true */2'use strict';34// Get dependencies5var Twitter = require('twitter');67// Set up Twitter client8var client = new Twitter({9 consumer_key: process.env.TWITTER_CONSUMER_KEY,10 consumer_secret: process.env.TWITTER_CONSUMER_SECRET,11 access_token_key: process.env.TWITTER_ACCESS_TOKEN_KEY,12 access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET13});1415// Set up connection to Redis16var redis;17if (process.env.REDIS_URL) {18 redis = require('redis').createClient(process.env.REDIS_URL);19} else {20 redis = require('redis').createClient();21}2223client.stream('statuses/filter', {track: 'javascript', lang: 'en'}, function(stream) {24 stream.on('data', function(tweet) {25 // Log it to console26 console.log(tweet);2728 // Publish it29 redis.publish('tweets', JSON.stringify(tweet));3031 // Persist it to a Redis list32 redis.rpush('stream:tweets', JSON.stringify(tweet));33 });3435 // Handle errors36 stream.on('error', function (error) {37 console.log(error);38 });39});
Most of this file should be fairly straightforward. We set up our connection to Twitter (you'll need to set the various environment variables listed here using the appropriate method for your operating system), and a connection to Redis.
We then stream the Twitter statuses that match our filter. When we receive a tweet, we log it to the console (feel free to comment this out in production if desired), publish it to a Redis channel called tweets
, and push it to the end of a Redis list called stream:tweets
. When an error occurs, we output it to the console.
Let's use Bootstrap to style the app. Create the following .bowerrc
file:
1{2 "directory": "static/bower_components"3}
Then run bower init
to create your bower.json
file, and install Bootstrap with bower install --save sass-bootstrap
.
With that done, create the file src/sass/style.scss
and enter the following:
1@import "compass/css3/user-interface";2@import "compass/css3";3@import "../../static/bower_components/sass-bootstrap/lib/bootstrap.scss";
This includes some dependencies from Compass, as well as Bootstrap. We won't be using any of the Javascript features of Bootstrap, so we don't need to worry too much about that.
Next, we need to create our view files. As React will be used to render the main part of the page, these will be very basic, with just the header, footer, and a section where the content can be rendered. First, create views/index.hbs
:
1___HANDLEBARS0___2 <div class="container">3 <div class="row">4 <div class="col-md-12">5 <div id='view'>___HANDLEBARS1___</div>6 </div>7 </div>8 </div>9 <script id="initial-state" type="application/json">___HANDLEBARS2___</script>10___HANDLEBARS3___
As promised, this a very basic layout. Note the markup
variable, which is where the markup generated by React will be inserted when rendered on the server, and the state
variable, which will contain the JSON representation of the data used to generate that markup. By passing that data through, you can ensure that the instance of React on the client has access to the same raw data as was passed through to the view on the server side, so that when the data needs to be re-rendered, it can be done so correctly.
We'll also define partials for the header and footer. The header should be in views/partials/header.hbs
:
1<!DOCTYPE html>2<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->3<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->4<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->5<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->6 <head>7 <meta charset="utf-8">8 <meta http-equiv="X-UA-Compatible" content="IE=edge">9 <title>Tweet Stream</title>10 <meta name="description" content="">11 <meta name="viewport" content="width=device-width, initial-scale=1">1213 <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->1415 <link rel="stylesheet" type="text/css" href="/css/style.css">16 </head>17 <body>18 <!--[if lt IE 7]>19 <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>20 <![endif]-->21 <nav class="navbar navbar-inverse navbar-static-top" role="navigation">22 <div class="container-fluid">23 <div class="navbar-header">24 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#header-nav">25 <span class="icon-bar"></span>26 <span class="icon-bar"></span>27 <span class="icon-bar"></span>28 </button>29 <a class="navbar-brand" href="/">Tweet Stream</a>30 <div class="collapse navbar-collapse navbar-right" id="header-nav">31 </div>32 </div>33 </div>34 </nav>
The footer should be in views/partials/footer.hbs
:
1 <script src="/jsx/bundle.js"></script>2 </body>3</html>
Note that we load the Javascript file /jsx/bundle.js
- this is the output from the command gulp react
.
Creating the back end
The next step is to implement the back end of the website. Add the following code as index.js
:
1/*jslint node: true */2'use strict';34require('babel/register');56// Get dependencies7var express = require('express');8var app = express();9var compression = require('compression');10var port = process.env.PORT || 5000;11var base_url = process.env.BASE_URL || 'http://localhost:5000';12var hbs = require('hbs');13var morgan = require('morgan');14var React = require('react');15var Tweets = React.createFactory(require('./components/tweets.jsx'));1617// Set up connection to Redis18var redis, subscribe;19if (process.env.REDIS_URL) {20 redis = require('redis').createClient(process.env.REDIS_URL);21 subscribe = require('redis').createClient(process.env.REDIS_URL);22} else {23 redis = require('redis').createClient();24 subscribe = require('redis').createClient();25}2627// Set up templating28app.set('views', __dirname + '/views');29app.set('view engine', "hbs");30app.engine('hbs', require('hbs').__express);3132// Register partials33hbs.registerPartials(__dirname + '/views/partials');3435// Set up logging36app.use(morgan('combined'));3738// Compress responses39app.use(compression());4041// Set URL42app.set('base_url', base_url);4344// Serve static files45app.use(express.static(__dirname + '/static'));4647// Render main view48app.get('/', function (req, res) {49 // Get tweets50 redis.lrange('stream:tweets', 0, -1, function (err, tweets) {51 if (err) {52 console.log(err);53 } else {54 // Get tweets55 var tweet_list = [];56 tweets.forEach(function (tweet, i) {57 tweet_list.push(JSON.parse(tweet));58 });5960 // Render page61 var markup = React.renderToString(Tweets({ data: tweet_list.reverse() }));62 res.render('index', {63 markup: markup,64 state: JSON.stringify(tweet_list)65 });66 }67 });68});6970// Listen71var io = require('socket.io')({72}).listen(app.listen(port));73console.log("Listening on port " + port);7475// Handle connections76io.sockets.on('connection', function (socket) {77 // Subscribe to the Redis channel78 subscribe.subscribe('tweets');7980 // Handle receiving messages81 var callback = function (channel, data) {82 socket.emit('message', data);83 };84 subscribe.on('message', callback);8586 // Handle disconnect87 socket.on('disconnect', function () {88 subscribe.removeListener('message', callback);89 });90});
Let's go through this bit by bit:
1/*jslint node: true */2'use strict';34require('babel/register');
Here we're using Babel, which is a library that allows you to use new features in Javascript even if the interpreter doesn't support it. It also includes support for JSX, allowing us to require JSX files in the same way we would require Javascript files.
1// Get dependencies2var express = require('express');3var app = express();4var compression = require('compression');5var port = process.env.PORT || 5000;6var base_url = process.env.BASE_URL || 'http://localhost:5000';7var hbs = require('hbs');8var morgan = require('morgan');9var React = require('react');10var Tweets = React.createFactory(require('./components/tweets.jsx'));
Here we include our dependencies. Most of this will be familiar if you've used Express before, but we also use React to create a factory for a React component called Tweets
.
1// Set up connection to Redis2var redis, subscribe;3if (process.env.REDIS_URL) {4 redis = require('redis').createClient(process.env.REDIS_URL);5 subscribe = require('redis').createClient(process.env.REDIS_URL);6} else {7 redis = require('redis').createClient();8 subscribe = require('redis').createClient();9}1011// Set up templating12app.set('views', __dirname + '/views');13app.set('view engine', "hbs");14app.engine('hbs', require('hbs').__express);1516// Register partials17hbs.registerPartials(__dirname + '/views/partials');1819// Set up logging20app.use(morgan('combined'));2122// Compress responses23app.use(compression());2425// Set URL26app.set('base_url', base_url);2728// Serve static files29app.use(express.static(__dirname + '/static'));
This section sets up the various dependencies of our app. We set up two connections to Redis - one for handling subscriptions, the other for reading from Redis in order to populate the view.
We also set up our views, logging, compression of the HTTP response, a base URL, and serving static files.
1// Render main view2app.get('/', function (req, res) {3 // Get tweets4 redis.lrange('stream:tweets', 0, -1, function (err, tweets) {5 if (err) {6 console.log(err);7 } else {8 // Get tweets9 var tweet_list = [];10 tweets.forEach(function (tweet, i) {11 tweet_list.push(JSON.parse(tweet));12 });1314 // Render page15 var markup = React.renderToString(Tweets({ data: tweet_list.reverse() }));16 res.render('index', {17 markup: markup,18 state: JSON.stringify(tweet_list)19 });20 }21 });22});
Our app only has a single view. When the root is loaded, we first of all fetch all of the tweets stored in the stream:tweets
list. We then convert them into an array of objects.
Next, we render the Tweets
component to a string, passing through our list of tweets, and store the resulting markup. We then pass through this markup and the string representation of the list of tweets to the template.
1// Listen2var io = require('socket.io')({3}).listen(app.listen(port));4console.log("Listening on port " + port);56// Handle connections7io.sockets.on('connection', function (socket) {8 // Subscribe to the Redis channel9 subscribe.subscribe('tweets');1011 // Handle receiving messages12 var callback = function (channel, data) {13 socket.emit('message', data);14 };15 subscribe.on('message', callback);1617 // Handle disconnect18 socket.on('disconnect', function () {19 subscribe.removeListener('message', callback);20 });21});
Finally, we set up Socket.io. On a connection, we subscribe to the Redis channel tweets
. When we receive a tweet from Redis, we emit that tweet so that it can be rendered on the client side. We also handle disconnections by removing our Redis subscription.
Creating our React components
Now it's time to create our first React component. We'll create a folder called components
to hold all of our component files. Our first file is components/index.jsx
:
1var React = require('react');2var Tweets = require('./tweets.jsx');34var initialState = JSON.parse(document.getElementById('initial-state').innerHTML);56React.render(7 <Tweets data={initialState} />,8 document.getElementById('view')9);
First of all, we include React and the same Tweets
component we require on the server side (note that we need to specify the .jsx
extension). Then we fetch the initial state from the script tag we created earlier. Finally we render the Tweets
components, passing through the initial state, and specify that it should be inserted into the element with an id of view
. Note that we store the initial state in data
- inside the component, this can be accessed as this.props.data
.
This particular component is only ever used on the client side - when we render on the server side, we don't need any of this functionality since we insert the markup into the view
element anyway, and we don't need to specify the initial data in the same way.
Next, we define the Tweets
component in components/tweets.jsx
:
1var React = require('react');2var io = require('socket.io-client');3var TweetList = require('./tweetlist.jsx');4var _ = require('lodash');56var Tweets = React.createClass({7 componentDidMount: function () {8 // Get reference to this item9 var that = this;1011 // Set up the connection12 var socket = io.connect(window.location.href);1314 // Handle incoming messages15 socket.on('message', function (data) {16 // Insert the message17 var tweets = that.props.data;18 tweets.push(JSON.parse(data));19 tweets = _.sortBy(tweets, function (item) {20 return item.created_at;21 }).reverse();22 that.setProps({data: tweets});23 });24 },25 getInitialState: function () {26 return {data: this.props.data};27 },28 render: function () {29 return (30 <div>31 <h1>Tweets</h1>32 <TweetList data={this.props.data} />33 </div>34 )35 }36});3738module.exports = Tweets;
Let's work our way through each section in turn:
1var React = require('react');2var io = require('socket.io-client');3var TweetList = require('./tweetlist.jsx');4var _ = require('lodash');
Here we include React and the Socket.io client, as well as Lodash and our TweetList component. With React.js, it's recommend that you break up each individual part of your interface into a single component - here Tweets
is a wrapper for the tweets that includes a heading. TweetList
will be a list of tweets, and TweetItem
will be an individual tweet.
1var Tweets = React.createClass({2 componentDidMount: function () {3 // Get reference to this item4 var that = this;56 // Set up the connection7 var socket = io.connect(window.location.href);89 // Handle incoming messages10 socket.on('message', function (data) {11 // Insert the message12 var tweets = that.props.data;13 tweets.push(JSON.parse(data));14 tweets = _.sortBy(tweets, function (item) {15 return item.created_at;16 }).reverse();17 that.setProps({data: tweets});18 });19 },20
Note the use of the componentDidMount
method - this fires when a component has been rendered on the client side for the first time. You can therefore use it to set up events. Here, we're setting up a callback so that when a new tweet is received, we get the existing tweets (stored in this.props.data
, although we copy this
to that
so it works inside the callback), push the tweet to this list, sort it by the time created, and set this.props.data
to the new value. This will result in the tweets being re-rendered.
1 getInitialState: function () {2 return {data: this.props.data};3 },
Here we set the initial state of the component - it sets the value of this.state
to the object passed through. In this case, we pass through an object with the attribute data
defined as the value of this.props.data
, meaning that this.state.data
is the same as this.props.data
.
1 render: function () {2 return (3 <div>4 <h1>Tweets</h1>5 <TweetList data={this.props.data} />6 </div>7 )8 }9});1011module.exports = Tweets;
Here we define our render
function. This can be thought of as our template. Note that we include TweetList
inside our template and pass through the data. Afterwards, we export Tweets
so it can be used elsewhere.
Next, let's create components/tweetlist.jsx
:
1var React = require('react');2var TweetItem = require('./tweetitem.jsx');34var TweetList = React.createClass({5 render: function () {6 var that = this;7 var tweetNodes = this.props.data.map(function (item, index) {8 return (9 <TweetItem key={index} text={item.text}></TweetItem>10 );11 });12 return (13 <ul className="tweets list-group">14 {tweetNodes}15 </ul>16 )17 }18});1920module.exports = TweetList;
This component is much simpler - it only has a render
method. First, we get our individual tweets and for each one define a TweetItem
component. Then we create an unordered list and insert the tweet items into it. We then export it as TweetList
.
Our final component is the TweetItem
component. Create the following file at components/tweetitem.jsx
:
1var React = require('react');23var TweetItem = React.createClass({4 render: function () {5 return (6 <li className="list-group-item">{this.props.text}</li>7 );8 }9});1011module.exports = TweetItem;
This component is quite simple. It's just a single list item with the text set to the value of the tweet's text
attribute.
That should be all of our components done. Time to compile our Sass and run Browserify:
1$ gulp compass2$ gulp react
Now, if you make sure you have set the appropriate environment variables, and then run node worker.js
in one terminal, and node index.js
in another, and visit http://localhost:5000/, you should see your Twitter stream in all its glory! You can also try it with Javascript disabled, or in a text-mode browser such as Lynx, to demonstrate that it still renders the page without having to do anything on the client side - you're only missing the constant updates.
Wrapping up
I hope this gives you some idea of how you can easily use React.js on both the client and server side to make web apps that are fast and search-engine friendly while also being easy to update dynamically. You can find the source code on GitHub.
Hopefully I'll be able to publish some later tutorials that build on this to show you how to build more substantial web apps with React.