Building a live news feed app

In this tutorial, we will see how to use the Ably Realtime client library to build a realtime news feed with React. Ably enables realtime data sharing using Pub/Sub messaging architecture via a concept called channels.

For the purpose of this tutorial, we’ll not discuss storage of messages or other server-side mechanisms.

React is a JavaScript library for building user interfaces. It is component-based and declarative which makes it easy to build complex UI with it.

Step 1 – Create your Ably app and API key

To follow this tutorial, you will need an Ably account. Sign up for a free account if you don’t already have one.

Access to the Ably global messaging platform requires an API key for authentication. API keys exist within the context of an Ably application and each application can have multiple API keys so that you can assign different capabilities and manage access to channels and queues.

You can either create a new application for this tutorial, or use an existing one.

To create a new application and generate an API key:

  1. Log in to your Ably account dashboard
  2. Click the “Create New App” button
  3. Give it a name and click “Create app”
  4. Copy your private API key and store it somewhere. You will need it for this tutorial.

To use an existing application and API key:

  1. Select an application from “Your apps” in the dashboard
  2. In the API keys tab, choose an API key to use for this tutorial. The default “Root” API key has full access to capabilities and channels.
  3. Copy the Root API key and store it somewhere. You will need it for this tutorial.

    Copy API Key screenshot

Step 2 – Create a React App

Prerequisites:

  • Make sure Node and NPM are installed on your system.
  • You do not need to install React separately as we’ll be using NPX (that comes bundled with NPM) for doing this.

We’ll start by creating a new React app. To do this, we’ll be using the create-react-app command that allows you to create a React app without having to worry about build configurations.

We’ll call the app newsfeed-app. Let’s create the app from the terminal by running:

npx create-react-app newsfeed-app

The new React app is ready, we can run it locally as follows:

cd newsfeed-app && npm start

This builds the static site, runs a web server and opens a browser. If your browser does not open automatically, navigate to http://localhost:3000 to open it up manually.

We will be using Semantic UI which is a UI component framework, to build UI easily and quickly. Let’s install that package as well as one for ably, by running the following command from the terminal:

npm install --save ably semantic-ui-react semantic-ui-css

See this step in Github

Step 3 – Update CSS

The semantic-ui-css contains the CSS for the Semantic UI framework. We’ll need to add an import statement to index.js file to enable use of semantic-ui-css:

import 'semantic-ui-css/semantic.min.css'

We need to add some CSS to style the HTML elements. You can simply replace the auto-filled contents in your App.css with the following:

.App-header {
  margin-top: 10px;
}

.App-button {
  margin-top: 5px;
  float: right;
}

.App-users {
  margin-top: 10px;
  text-align: left;
}

.App-update {
  margin-bottom: 10px;
  cursor: pointer;
  text-align: center;
}

.App-cards {
  overflow-y: scroll;
  max-height: 500px;
}

Step 4 – Add Publish/Subscribe mechanism using Ably

We’ll get started with a few import statements, add the following to your App.js file:

import React, { useState, useEffect } from "react";
import * as Ably from 'ably';
import './App.css';

In our newsfeed app, we would like to display the profile information of each user who is publishing a new update. Since we are not including any database and such for this tutorial, we’ll simply assign a random avatar and random name to each user who is using an instance of the application.

So, let’s add two arrays, one each for avatars(hosted image URLs) and names to be assigned randomly to the users who log in. We’ll also add a random function to pick an item from these arrays. Feel free to replace them with any other images and names that you like:

// User avatars
const avatarsInAssets = [
 "https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_8.png?1536042504672",
 "https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_3.png?1536042507202",
 "https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_6.png?1536042508902",
 "https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_10.png?1536042509036",
 "https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_7.png?1536042509659",
 "https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_9.png?1536042513205",
 "https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_2.png?1536042514285",
 "https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_1.png?1536042516362",
 "https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_4.png?1536042516573",
 "https://cdn.glitch.com/0bff6817-d500-425d-953c-6424d752d171%2Favatar_5.png?1536042517889"
];

// User names
const names = [
  "Ross",
  "Monica",
  "Rachel",
  "Joey",
  "Chandler",
  "Steve",
  "Bill",
  "Elon",
  "Tom",
  "Shaun"
];

// Get random
function getRandomArbitrary(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
}

Now, let’s instantiate the Ably realtime library as well as the channels that we’ll use to share realtime data. Remember to replace ‘’ with your own API key value. We’ll also assign a name and avatar to the current user:

const myId = "id-" + Math.random().toString(36).substr(2, 16);
const apiKey = '<YOUR-API-KEY>';
// Ably Instance
const ably = new Ably.Realtime({
    key: apiKey,
    clientId: myId,
    echoMessages: false
});
const chatChannel = ably.channels.get("chat");
const presenceChannel = ably.channels.get("presence");
let my = {};
my.avatar = avatarsInAssets[getRandomArbitrary(0, 9)];
my.name = names[getRandomArbitrary(0, 9)]

We have passed a few client options when instantiating Ably:

  • key is the API key
  • clientId is used to uniquely identify a client within an Ably app
  • echoMessages if false, prevents messages originating from this connection being echoed back on the same connection

Channels are the medium through which messages are distributed; clients attach to channels to subscribe to messages, and every message published to that channel is broadcast by Ably to all its subscribers.

Presence enables clients to be aware of other clients that are currently “present” on a channel, i.e, connected or online.

Now, let’s make the functional App component and states within it to hold the user input as well as the updates coming in:

const App = () => {
  const [todoInput, setTodoInput] = useState(""); // User Input/Message
  const [state, setState] = useState({
    msgs: [],
    newMsgs: []
  }); // to hold all messages and incoming new messages

  let you = {};
}

Subscribing to a channel would let the client automatically receive all updates published on that channel by other clients, in realtime. So, let’s subscribe to the channels in the useEffect hook on mount of the App component:

useEffect(() => {
        // Subscribing for userInfo details to assign name and avatar
        chatChannel.subscribe("userInfo", (data) => {
            var dataObj = JSON.parse(JSON.stringify(data));
            if (dataObj.clientId !== myId) {
                you.avatar = dataObj.data.avatar;
                you.name = dataObj.data.name;
            }
        });

        // When a new member joins publish it's userInfo details
        presenceChannel.presence.subscribe('enter', function (member) {
            if (member.clientId !== myId) {
                chatChannel.publish("userInfo", {
                    "avatar": my.avatar,
                    "name": my.name
                });
            }
        });

        presenceChannel.presence.enter();

        // Get all members and publish userInfo details
        presenceChannel.presence.get(function (err, members) {
            for (var i in members) {
                if (members[i].clientId !== myId) {
                    chatChannel.publish("userInfo", {
                        "avatar": my.avatar,
                        "name": my.name
                    });
                }
            }
        });

        // Subscribing for chatMessage
        chatChannel.subscribe("chatMessage", (data) => {
            var dataObj = JSON.parse(JSON.stringify(data));
            var message = dataObj.data.message;
            var date = new Date(dataObj.data.date);

            // Updating the state newMsgs with new incoming message
            setState(prevState => {
              return {
                ...prevState,
                newMsgs: [
                  ...prevState.newMsgs,
                  { summary: message, image: you.avatar, date: date, name: you.name }
                ]
              };
            });
        })

      }, []);

As you can see we subscribe to many channels and the publish our data. Let’s discuss each of them in more detail:

  • We are using the presenceChannel to share the connection info or online presence of a user. A user can set themselves as connected by entering the presence channel. We do this by calling the enter method on that channel.
  • Next, we use another channel, i.e. the chatChannel to publish the user info.
  • We also need to subscribe to the same chatChannel to receive the userInfo from other users. In most cases, you would subscribe to a channel before publishing to it if you are intending to receive back your own messages as well.
  • Lastly, in the same chatChannel we subscribe to the chatMessage event to get the updates to be displayed in the news feed. We then update the state variables with this new update.

Now, add the following imports statements on top of the file App.js after the Ably import statement to enable use of Semantic-UI:

import {
  Container,
  Header,
  Divider,
  Button,
  Form,
  TextArea
} from "semantic-ui-react";

Next, let’s add the sendMyMessage function to the App component and the corresponding HTML to post an update:

function sendMyMessage() {
        const newMsgId = "msg-id-" + Math.random().toString(36).substr(2, 6);
        setTodoInput("");
        if (todoInput !== "") {
            // Publishing the message
            chatChannel.publish("chatMessage", {
                message: todoInput,
                localMsgId: newMsgId,
                date: new Date()
            });

            // Updating the state msgs with the message of the current user and sorting
            setState(prevState => {
              return {
                ...prevState,
                msgs: [
                  ...prevState.msgs,
                  {
                    summary: todoInput,
                    image: my.avatar,
                    date: new Date(),
                    name: my.name
                  }
                ].sort((a, b) => b.date - a.date)
              };
           });
        }
    }

    return (
        <Container textAlign='left' className='App-header'>
          <Header as='h2'>Ably React Tutorial - RealTime</Header>
          <Form>
             <TextArea
                value={todoInput}
                placeholder="Post an update"
                onChange={e => setTodoInput(e.target.value)}
             />
             <div className="App-button">
               <Button content="Send" primary onClick={sendMyMessage} />
             </div>
         </Form>

         <Divider />

         </Container>
    )

As you can see in the sendMyMessage function, we publish the chatMessage event on the chatChannel channel and update the local state accordingly with the new update posted by the user.

See this step in Github

Step 5 – Displaying the updates in the news feed

Now that we have the data to be displayed in the news feed, let us display this in the UI. We’ll start by adding the following imports statements in App.js file:

import React, { useState, useEffect, useRef } from "react";
import {
  Container,
  Header,
  Divider,
  Button,
  Form,
  TextArea,
  Label
} from "semantic-ui-react";
import UICard from "./UICard";

Next, let’s add a mechanism to reset the scroll to the top of the newsfeed. Add the following on the top of the App component above the todoInput state:

const scrollRef = useRef(null); // for getting scroll ref
const [todoInput, setTodoInput] = useState(""); // User Input/Message

Next, add the function getMessages() below the sendMyMessage() function we previously added, to update the news feed with the new updates:

function getMessages() {
    // Updating the state with latest messages and sorting
    setState(prevState => {
      return {
        ...prevState,
        msgs: [...prevState.msgs, ...prevState.newMsgs].sort(
          (a, b) => b.date - a.date
        ),
        newMsgs: []
      };
    });

    // Scrolling to top for viewing new messages
    scrollRef.current.scrollTop = 0;
}

We’ll display each update in its own card style structure. So, let’s create a new file called UICard.js in the src folder and add the following code:

import React from 'react'
import { Card, Image, Segment } from 'semantic-ui-react'

// Formatting date
function formatAMPM(date) {
  var hours = date.getHours();
  var minutes = date.getMinutes();
  var ampm = hours >= 12 ? 'PM' : 'AM';
  hours %= 12;
  hours = hours ? hours : 12;
  minutes = minutes < 10 ? '0' + minutes : minutes;
  var strTime = hours + ':' + minutes + ' ' + ampm;
  return strTime;
}

const UICard = (props) => {
  return (
    <Segment>
{props.events.map((event, index) => {
  return (
    <Card fluid key={index} index={index}>
      <Card.Content>
        <Image
          floated='right'
          size='mini'
          src={event.image}
        />
        <Card.Header>{event.name}</Card.Header>
        <Card.Meta>{formatAMPM(event.date)}</Card.Meta>
        <Card.Description>
          {event.summary}
        </Card.Description>
      </Card.Content>
    </Card>
     )
    })}
  </Segment>
)
  }

export default UICard

The formatAMPM function is used to display the date in readable format. Using the props the UICard component displays the news feed.

Now, let’s update the return method of the App component to use UICard component to display the news feed:

return (
    <Container textAlign="left" className="App-header">
      <Header as="h2">Ably React Tutorial - RealTime</Header>
      <Form>
        <TextArea
          value={todoInput}
          placeholder="Post an update"
          onChange={e => setTodoInput(e.target.value)}
        />
        <div className="App-button">
          <Button content="Send" primary onClick={sendMyMessage} />
        </div>
      </Form>

      <Divider />
      <div onClick={getMessages} className="App-update">
        {state.newMsgs.length !== 0 && (
          <Label>
            {state.newMsgs.length.toString()} new update
            {state.newMsgs.length === 1 ? "" : "s"} have arrived!
          </Label>
        )}
      </div>
      {state.msgs.length !== 0 && (
        <div ref={scrollRef} className="App-cards">
          <UICard events={state.msgs} />
        </div>
      )}
    </Container>
  );

See this step in Github

Step 6 – Retrieving online users using Ably’s Channel Occupancy API

Ably’s offers some special channels called Metachannels to share information about the channels themselves in a given app. These channels always start with the [meta] qualifier, separating them from the regular channels used for realtime data transfer.

Among other things, the metadata that is of interest to us is the channel occupancy event updates which are published on the metachannel identified by [meta]channel.lifecycle. We will use the channel occupancy here to get active connections. Please note that channel occupancy event updates are an enterprise only feature. You can still use the Channel Status API to poll Ably for meta info about the channels in an app.

Let’s add a subscription to this metachannel. Add it below the presenceChannel variable:

const presenceChannel = ably.channels.get("presence");
const metaChannel = ably.channels.get("[meta]channel.lifecycle");

Now, add a state variable to hold the online users’ count, in the App component:

const [onlineUsers, setOnlineUsers] = useState(0); // for online users

Now let’s add the subscribe method on the metaChannel channel in the useEffect hook:

// Using Channel Occupancy to get online users
metaChannel.subscribe("channel.occupancy", msg => {
      var msgJSONobj = JSON.parse(JSON.stringify(msg.data));
      if (msgJSONobj.name === "chat") {
        // Update the state OnlineUsers
        setOnlineUsers(msgJSONobj.status.occupancy.metrics.connections);
      }
});

So using the Channel Occupancy API response we set the onlineUsers variable to hold the number of online users.

Now, update the return method of the App component to show the online users count:

</Form>
      <div className="App-users">
        {onlineUsers !== 0 && (
          <Label>
            User{onlineUsers === 1 ? "" : "s"} Online - {onlineUsers.toString()}
          </Label>
        )}
      </div>
<Divider />

See this step in Github

That’s it! We now have a news feed that updates in realtime. To test it out, you can run the app locally from the terminal:

npm start

Then open http://localhost:3000 in two different browser tabs. Post an update in one of the opened tabs and watch the other tab update in realtime.

Download tutorial source code

The complete source code for each step of this tutorial is available on Github.

We recommend that you clone the repo locally:

git clone https://github.com/ably/tutorials.git

Each tutorial’s source code is hosted on a separate branch. Checkout the one for this tutorial:

git checkout react-newsfeed-app

Install the NPM dependencies:

cd react-newsfeed-app && npm install

And then run the app locally by adding your Ably API key to src/App.js and run

npm start

to start the web server and open the browser.

Live demo

React news feed app live demo!

To try this example yourself, Open this demo on a new tab to see news feed app in action.

Next steps

1. If you would like to find out more about how channels and how publishing & subscribing works, see the realtime channels & messages documentation
2. Learn more about Channel Occupancy Events
3. Learn more about Ably features by stepping through our other Ably tutorials
4. Learn more about React
5. Learn more about Ably’s history feature to understand how you can prevent your clients from losing messages.
6. Gain a good technical overview of how the Ably realtime platform works
7. Get in touch if you need help