Building a Realtime Commenting App

In this tutorial, you will learn how to use Ably’s Realtime client library to build a live commenting web app. When a website visitor leaves a comment, you will publish the comment to an Ably channel and also subscribe to that channel to see comments as they are added in realtime.

You will use React: a component-based, declarative JavaScript library, to build the user interface.

React logo

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

Start by creating a new React app. For simplicity, use Create React App, which enables you to create a single-page React app without having to worry about build configurations.

Install Create React App using npm:

npm install -g create-react-app

Create a new React app called reactjs-realtime-commenting:

create-react-app reactjs-realtime-commenting

Test your new React app. First, change into the reactjs-realtime-commenting@ directory:

cd reactjs-realtime-commenting

Then, execute npm start:

npm start

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

See this step in Github

Step 3 – Delete unused files

You won’t need the src/App.css, src/App.test.js, and src/logo.svg files, so delete them:

rm src/App.css src/App.test.js src/logo.svg

See this step in Github

Step 4 – Create the components folder

Create a new folder within the src folder called src/components. This folder will contain all the React components you will build in this tutorial:

mkdir src/components

Move the existing App.js file into the src/components folder:

mv src/App.js src/components

See this step in Github

Step 5 – Remove references to unused files

The files you moved or deleted earlier are still referenced by the src/index.js and src/components/App.js files, so you must update these references.

First, update the location of App in src/index.js:

import React from "react"
import ReactDOM from "react-dom"
import "./index.css"
import App from "./components/App" // New location of App.js
import reportWebVitals from "./reportWebVitals"

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
)

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

Then, replace the contents of src/components/App.js with the following code, which creates a new React component called App:

import React, { Component } from "react"

class App extends Component {
  render() {
    return <div className="App"></div>
  }
}

export default App

See this step in Github

Step 6 – Add the Bulma CSS framework

Use the Bulma CSS framework so that you can apply some simple styling in a later step. Add the following line to the <head> section of the public/index.html file:

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css"
/>

See this step in Github

Step 7 – Install and configure the Ably realtime client library

In this step you will install the Realtime client library so that you can publish and subscribe to comments in realtime. You will then instantiate it using your API key, and make the instance accessible from multiple places within your application.

Note: This tutorial uses basic authentication for simplicity. In production applications, Ably recommends that you use token authentication for browser clients.

First, install the client library:

npm install ably

Then, create a file called .env in the root of your application directory and use it to configure your Ably API key:

REACT_APP_ABLY_API_KEY=your-api-key-goes-here

Finally, within the src/components folder, create a new file named Ably.js and populate it with the following code:

import { Realtime } from "ably"

export default new Realtime(process.env.REACT_APP_ABLY_API_KEY)

See this step in Github

Step 8 – Build the comment form

Within the src/components folder, create a new file named CommentBox.js and populate it with the following code:

import React, { Component } from "react"

class CommentBox extends Component {
  constructor(props) {
    super(props)
  }
  render() {
    return (
      <div>
        <h1 className="title">Please leave your feedback below</h1>
        <form onSubmit={this.addComment}>
          <div className="field">
            <div className="control">
              <input
                type="text"
                className="input"
                name="name"
                placeholder="Your name"
              />
            </div>
          </div>
          <div className="field">
            <div className="control">
              <textarea
                className="textarea"
                name="comment"
                placeholder="Add a comment"
              ></textarea>
            </div>
          </div>
          <div className="field">
            <div className="control">
              <button className="button is-primary">Submit</button>
            </div>
          </div>
        </form>
      </div>
    )
  }
}

export default CommentBox

This component renders a comment form. Submitting the form triggers an onSubmit event which in turn calls addComment(), which doesn’t exist yet. In the next step you will create the addComment() method.

See this step in Github

Step 9 – Publish comments

Within src/components/CommentBox.js, add the following code just before the render() method:

addComment(e) {
  // Prevent the default behaviour of form submit
  e.preventDefault()
  // Get the value of the comment box
  // and make sure it not some empty strings
  const comment = e.target.elements.comment.value.trim()
  const name = e.target.elements.name.value.trim()
  // Get the current time.
  const timestamp = Date.now()
  // Make sure name and comment boxes are filled
  if (name && comment) {
    const commentObject = { name, comment, timestamp }
    // Publish comment
    const channel = Ably.channels.get("comments")
    channel.publish("add_comment", commentObject, (err) => {
      if (err) {
        console.log("Unable to publish message err = " + err.message)
      }
    })
    // Clear input fields
    e.target.elements.name.value = ""
    e.target.elements.comment.value = ""
  }
}

The above code:

  1. Prevents the default form submission behaviour (reloading the page)
  2. Validates the form inputs
  3. Publishes valid comments to the comments channel, using the add_comment topic
  4. Clears the form inputs ready for the next comment

To work with Ably channels and messages, you must access the Ably realtime client library instance you instantiated in src/components/Ably.js, so add the following to the list of import statements at the top of the CommentBox.js file:

import Ably from "./Ably"

Then, bind addComment() to the current object instance by adding the following line to the class constructor (just after the call to super()):

this.addComment = this.addComment.bind(this)

See this step in Github

Step 10 – Render a comment

Within the src/components folder, create a new file named Comment.js and add the following code:

import React, { Component } from 'react'

class Comment extends Component {
  constructor(params) {
    super(params)
    this.messageDate = this.messageDateGet()
  }
  messageDateGet() {
    const date = new Date(this.props.comment.timestamp)
    const dateTimeFormatOptions = {
      year: "2-digit",
      month: "2-digit",
      day: "2-digit",
      hour: "2-digit",
      minute: "2-digit",
    }
    const localeString = date.toLocaleString(undefined, dateTimeFormatOptions)
    return localeString
  }
  render() {
    return (
      <article className="media">
        <figure className="media-left">
          <p className="image is-64x64">
            <img src="https://bulma.io/images/placeholders/128x128.png" alt="Avatar" />
          </p>
        </figure>
        <div className="media-content">
          <div className="content">
            <span className="user-name">{this.props.comment.name} </span>
            <span className="message-date">{this.messageDate}</span>
            <p>{this.props.comment.comment}</p>
          </div>
        </div>
      </article>
    )
  }
}

export default Comment

This component renders a single comment that is supplied to it using props. In React, props are custom attributes that are used to pass data to components. The messageDateGet() function is a utility function for converting the message timestamp into a human-readable date and time format.

Currently, each comment is assigned the same blank placeholder image. You will fix this in the next step.

See this step in Github

Step 11 – Add an avatar image

In production you would want to associate a comment with a particular user and display that user’s avatar. In this tutorial you will simulate this by assigning a random dog picture from the Dog API to each comment. You will use axios to fetch each image.

First, install axios:

npm install axios

and import it in the CommentBox component (src/components/CommentBox.js):

import axios from "axios"

Mark the addComment() method as async and replace its contents as shown below. This code makes a HTTP request to the Dog API to retrieve the URL of a random dog picture and stores that URL in an object property called avatar:

// Make the addComment() function asychronous
  async addComment(e) {
    e.preventDefault()
    const comment = e.target.elements.comment.value.trim()
    const name = e.target.elements.name.value.trim()
    const timestamp = Date.now()

    // Retrieve a random image from the Dog API
    const avatar = await (
      await axios.get("https://dog.ceo/api/breeds/image/random")
    ).data.message

    if (name && comment) {
      // include the avatar image in the commentObject
      const commentObject = { name, comment, timestamp, avatar }
      console.log(commentObject)

      const channel = Ably.channels.get("comments")
      channel.publish("add_comment", commentObject, (err) => {
        if (err) {
          console.log("Unable to publish message err = " + err.message)
        }
      })
      e.target.elements.name.value = ""
      e.target.elements.comment.value = ""
    }
  }

Then, in the Comment component (src/components/Comment.js), use this dynamically-generated image instead of the placeholder:

render() {
  return (
    <article className="media">
      <figure className="media-left">
        <p className="image is-64x64">
          <img alt="dog pic" src={this.props.comment.avatar} />
        </p>
      </figure>
      <div className="media-content">
        <div className="content">
          <span className="user-name">{this.props.comment.name} </span>
          <span className="message-date">{this.messageDate}</span>
          <p>{this.props.comment.comment}</p>
        </div>
      </div>
    </article>
  )
}

See this step in Github

Step 12 – List all comments

You now need a way to manage all the incoming comments.

Within the src/components folder, create a new Comments component in a file named Comments.js, with the following code:

import React, { Component } from 'react'
import Comment from './Comment'

class Comments extends Component {
  render() {
    return (
      <section className="section">
        {
          this.props.comments.map((comment, index) => {
            return <Comment key={comment.timestamp} comment={comment} />
          })
        }
      </section>
    )
  }
}

export default Comments

The Comments component accepts a comments props and renders the Comment component once for each comment, using the props data.

See this step in Github

Step 13 – Create the App component

The App component will be the parent component for all the other components in your application. Replace the contents of the src/components/App.js file with the following code:

import React, { Component } from 'react'
import CommentBox from './CommentBox'
import Comments from './Comments'

class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      comments: []
    }
  }

  render() {
    return (
      <section className="section">
        <div className="container">
          <div className="columns">
            <div className="column is-half is-offset-one-quarter">
              <CommentBox />
              <Comments comments={this.state.comments} />
            </div>
          </div>
        </div>
      </section>
    )
  }
}

export default App

This contains the CommentBox and Comments components you created earlier. The comments state is an array of comments which is empty by default but updates as users add comments. The comments state is passed as props to the Comments component. This is how the Comments component receives the comments it renders.

See this step in Github

Step 14 – Update the application state with new comments

You need to add the new comment to the App component’s state so that the list of comments is updated in realtime. Make all the changes in this step to the src/components/App.js file.

First, add the following code just before the call to render():

handleAddComment(comment) {
  this.setState(prevState => {
    return {
      comments: [comment].concat(prevState.comments),
    }
  })
}

Then, bind handleAddComment() to the this keyword by adding this line to the constructor() just after the call to super():

this.handleAddComment = this.handleAddComment.bind(this)

See this step in Github

Step 15 – Configure channels to persist messages to disk

You’ll use Ably’s history feature to persist all comment messages to disk for later retrieval by clients. This capability must be configured on channels using channel rules. In this tutorial you will create a channel rule for all channels in the persisted namespace.

  1. Visit your account dashboard and select the same app you chose in Step 1 when obtaining your API key
  2. Click on the Settings tab and scroll down to the “Channel rules” section
  3. Click the “Add new rule” button:

    Add new channel rule screenshot
  4. In the “New Channel Rule” dialog box, enter “persisted” for the namespace, check the “Persist all messages” check box to enable history, and click the “Create channel rule” button:

    Create channel rule screenshot

You have now enabled history for all channels in the persisted namespace so that any channel with a name that matches the pattern persisted:* will store published messages to disk.

Step 16 – Display historical comments

Now you need to display the comments. You want these to update in realtime, so you must subscribe to the comments channel and process any incoming messages.

Even without enabling history, Ably will automatically persist messages for a short period. So when the app loads, we can display any previously persisted comments by using a React lifecycle componentDidMount() hook. In src/components/App.js, add the following code just before the handleAddComment() function.

componentDidMount() {
  const channel = Ably.channels.get("comments")

  channel.attach()
  channel.once("attached", () => {
    channel.history((err, page) => {
      // create a new array with comments in reverse order (old to new)
      const comments = Array.from(page.items, (item) => item.data)
      this.setState({ comments })
      channel.subscribe((msg) => {
        this.handleAddComment(msg.data)
      })
    })
  })
}

The componentDidMount() hook runs after the App component is inserted into the DOM. It is a good place to fetch comments from history. The code connects to the comments channel and listens for the attached event. It then updates the application state with the comments pulled from history.

The code then subscribes to new messages on the channel. When a new message is sent on the channel, it uses the message data to add a new comment using handleAddComment().

For this to work you need to import your Ably component. Add the following line to the top of the src/components/App.js file under the other import statements:

import Ably from "./Ably"

Finally, replace the contents of src/index.css with the following CSS, to format the display of the comments:

.user-name {
  font-weight: bold;
  font-size: 16px;
}
.message-date {
  font-size: 13px;
  color: #afacac;
}
.user-name + .message-date {
  margin-left: 10px;
}

You should now have a realtime commenting system. To test it out, start the app:

npm start

Then, open http://localhost:3000 in two different browser tabs. Add a comment in one of the opened tabs and watch the other tab update with the comment in realtime.


Final output screenshot

See this step in Github

Step 17 (optional) – Add a profanity filter

To avoid displaying rude or offensive words that your users might put into comments, you can use a module that filters such words, such as bad-words.

First, install the module using npm:

npm install bad-words

In src/components create new file called ProfanityFilter.js and add the following code:

import badWords from "bad-words"
const filter = new badWords()

function clean(textToFilter) {
 return filter.clean(textToFilter || '')
}

export default clean

This component exports a function which replaces bad words with asterisks (*). You can then use it in the comment rendering template for both the name and comment inputs.

In src/components/Comment.js add a new import:

import filterBadWords from "./ProfanityFilter"

In the same file, include the following lines in the constructor() function:

this.userName = filterBadWords(this.props.comment.name)
this.commentText = filterBadWords(this.props.comment.comment)

Then, replace the render() function with the following code:

render() {
  return (
    <article className="media">
      <figure className="media-left">
        <p className="image is-64x64">
          <img alt="dog pic" src={this.props.comment.avatar} />
        </p>
      </figure>
      <div className="media-content">
        <div className="content">
          <span className="user-name">{this.userName} </span>
          <span className="message-date">{this.messageDate}</span>
          <p>{this.commentText}</p>
        </div>
      </div>
    </article>
  )
}

Run npm start and use your imagination to test that the profanity filter works as expected!

See this step in Github

Download the 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

Checkout the tutorial branch:

git checkout reactjs-realtime-commenting

Install the required dependencies. Change into the project’s directory and then run this command in your terminal:

npm install

Create a .env file in the project’s directory and populate the REACT_APP_ABLY_API_KEY setting with your API key:

REACT_APP_ABLY_API_KEY=your-api-key

Run the app locally by executing:

npm start

This starts the web server and opens your browser.

Next steps

1. If you would like to find out more about how channels, publishing and subscribing works, see the Realtime channels & messages documentation
2. Learn more about Ably features by trying out our other tutorials
3. Learn more about Ably’s history feature
4. Gain a good technical overview of how the Ably realtime platform works
5. Get in touch if you need help