Real-Time Blog Post Views with React and Firebase

-

Blog Post Views

One metric for a website's success is the amount of traffic it receives. For a blog, this translates to page views. Articles with lots of views indicate the content your readers care most about. This helps drive future articles.

Most people measure view counts via analytics (like Google Analytics). However, with the rise of ad-blocking browser extensions, these counts are likely ~10% off (especially if your target market is tech). What if we could have better accuracy, better performance, and better privacy for our users?

Table of Contents

  1. Who Is This For?
  2. Technical Overview
  3. Setting Up Firebase
  4. Creating The Microservice
  5. Deploying and Testing View Counts
  6. Lazy-Loading Firebase Client Side
  7. Real-Time View Count Updates
  8. Conclusion

Who Is This for?

Anyone running their own blog, whether that's using vanilla React or a static-site generator like Gatsby or Next.js.

Technical Overview

Objective

Display a real-time view count for a given blog post. This should not block the page load and should log views asynchronously. The client should automatically update when it receives a new count from the server.

Server

To track views, we will be creating a serverless microservice with Micro and a Firebase Realtime Database. Given an ID query parameter (/?id=$id) our microservice should increment views for the corresponding post in Firebase. It should also prevent abuse by logging IPs.

Client

Our client application should lazy-load the Firebase JavaScript SDK on page load and subscribe to view counts. When it receives a new count, the counter should highlight and update.

We can host and deploy this entire solution for free using Firebase's free tier and Zeit's Now. Amazing.

Setting up Firebase

  1. If you do not have a Firebase account, create one first.
  2. Create a new project.
  3. Navigate to "Database" and click "Create Database". Create Firebase Database
  4. Start in test mode and click next. Create Firebase Database
  5. Choose your database location and click done. Create Firebase Database
  6. In the top left, click on "Project Settings". Create Firebase Database
  7. Navigate to "Service Accounts" tab and click "Generate new private key". Save the .json file. You will need this later. Create Firebase Database

We're finished! 🎉 You have successfully set up a realtime database, as well as generated credentials for your microservice to connect to the database.

Creating the Microservice

This blog post was inspired by Guillermo Rauch's blog, where he open-sourced his code. I've updated some things (to use Now 2.0 and React Hooks) but this is largely inspired by his work 🎉

You can view the entire source code here. I'll be explaining some key concepts for how the microservice works.

Connecting to Firebase

Before we can log views, we need to connect to our Firebase Realtime Database. Place your service-account.json in the root of the repo. This file is inside .gitignore so it won't be committed to GitHub. We can connect to the database using firebase-admin. Make sure you update the databaseURL.

const admin = require('firebase-admin');
const {join} = require('path');
const cert = join(__dirname, '../service-account.json');

admin.initializeApp({
    credential: admin.credential.cert(cert),
    databaseURL: 'https://your-blog.firebaseio.com'
});

module.exports = admin.database();

Tracking Views

To track a view, we need to look at the database for views -> id.

const db = require('./db');

module.exports = function incrementViews(id) {
    const ref = db.ref('views').child(id);
    return ref.transaction((currentViews) => {
        // if it has never been set it returns null
        if (currentViews === null) currentViews = 0;
        return currentViews + 1;
    });
};

Putting It All Together

The entry point into the application sets CORS headers, checks for a valid request, parses the id query parameter, and increments the value for the given id. Make sure to update the URL for your blog.

const verify = require('./lib/verify');
const {parse} = require('url');
const increment = require('./lib/increment-views');

module.exports = async (req, res) => {
    const orig = req.headers.origin;
    if (/https://(.*.)?leerob.io/.test(orig)) {
        res.setHeader('Access-Control-Allow-Origin', orig);
        res.setHeader('Access-Control-Allow-Methods', 'GET');
    }

    // ensure no duplicate views
    verify(req);
    const {
        query: {id}
    } = parse(req.url, true);

    if (!id) {
        const err = new Error('Missing `id` parameter');
        err.statusCode = 400;
        throw err;
    }

    const {snapshot} = await increment(id);
    return {total: snapshot.val()};
};

Deploying and Testing View Counts

After you've cloned the repo and followed the steps above to substitute your values, run yarn dev to start the microservice.

Using your favorite REST client or your browser, you can now hit http://localhost:3000?id=blog-post to log a view.

Testing Microservice

We can confirm it was logged correctly by checking Firebase.

Testing Firebase

Deployment via Now

We can configure our microservice to deploy via Now through a now.json file. Update the name and alias to be unique for your microservice.

{
    "version": 2,
    "name": "your-blog-views",
    "alias": "your-blog-views",
    "builds": [{"src": "index.js", "use": "now-micro"}]
}

After installing the Now CLI, you can deploy in one command 🚀

$ now --target production

You should now be able to hit https://your-blog-views.now.sh/.

Lazy-Loading Firebase Client Side

Firebase is a large module. In fact, it's larger than react, react-dom and next combined. We only want to load the parts of Firebase we need (firebase/app and firebase/database) and we don't want to block the page load.

Let's lazy-load the Firebase JavaScript SDK.

export default async function loadDb() {
    const firebase = await import('firebase/app');

    await import('firebase/database');

    try {
        firebase.initializeApp({
            databaseURL: 'https://your-blog.firebaseio.com'
        });
    } catch (error) {
        /*
         * We skip the "already exists" message which is
         * not an actual error when we're hot-reloading.
         */
        if (!/already exists/.test(error.message)) {
            console.error('Firebase initialization error', error.stack);
        }
    }

    return firebase.database().ref('views');
}

Real-Time View Count Updates

Now that our microservice is deployed, we can instruct our client to asynchronously log a view when the page loads. We also need to subscribe to the view counts for the given blog post ID so the client updates every time the value changes.

Let's create a <ViewCounter /> component.

Note: The <Views /> component will be created next.

import React, {useState, useEffect} from 'react';

import loadDb from '../lib/load-db';

import Views from './views';

function ViewCounter({id}) {
    const [views, setViews] = useState('');

    // Subscribe to view count updates
    useEffect(() => {
        const onViews = (newViews) => setViews(newViews.val());
        let db;

        const fetchData = async () => {
            db = await loadDb();
            db.child(id).on('value', onViews);
        };

        fetchData();

        return function cleanup() {
            db.child(id).off('value', onViews);
        };
    }, [id]);

    // Asynchronously log a view
    useEffect(() => {
        const registerView = () => fetch(`https://your-blog-views.now.sh/?id=${encodeURIComponent(id)}`);

        registerView();
    }, [id]);

    return <Views views={views} />;
}

export default ViewCounter;

Now, let's look at <Views />. This component should accept a prop views and display a formatted string that highlights when the value updates.

First, let's look at the styles. A highlight prop controls a CSS animation.

const highlightBackgound = keyframes`
    from {
        background-color: yellow;
    }
    to {
        background-color: #fff;
    }
`;

const StyledViews = styled.span`
    font-size: 0.9em;
    text-transform: uppercase;
    letter-spacing: 0.04em;

    ___CSS_0___
`;

When a new views prop is passed to the component, it sets the highlight state to true. After 2 seconds, it resets the highlight state.

import format from 'comma-number';
import React, {useState, useEffect} from 'react';
import styled, {css, keyframes} from 'styled-components';

function Views(props) {
    const [highlight, setHighlight] = useState(false);

    useEffect(() => {
        if (props.views) {
            setHighlight(true);
        }

        setTimeout(() => {
            setHighlight(false);
        }, 2000);
    }, [props.views]);

    const formattedViews = `${format(props.views)} views`;

    return (
        <Container>
            <StyledViews highlight={highlight}>{` - ${props.views && formattedViews}`}</StyledViews>
        </Container>
    );
}

export default Views;

Finally, we can consume the view counter in our blog post and pass in the ID.

<ViewCounter id="post-id" />

You can see a completed client-side example by checking out this blog post's source code.

Conclusion

It works! 🎉

Blog Post Views Example Gif

I was able to migrate my page views from Google Analytics into Firebase via their web interface.

Blog Post Views

Note: If your site receives a lot of traffic, you'll need to upgrade from the Spark plan (free) to Blaze (pay-as-you-go). On Spark, you can only have 100 simultaneous connections. With Blaze, you can have 100k.

Database Usage


Looking to Learn React & Next.js? 🏆

I've launched a new video course Mastering Next.js containing 55 jam-packed lessons. Pre-order now and get 50% off!

Mastering Next.js

Discuss on TwitterEdit on GitHub

Want More? Be Notified When I Post New Articles 🚀