Real-Time Blog Post Views With React and Firebase LR Lee Robinson / August 21, 2019 8 min read • ––– 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 #

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.

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.

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.

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 Vercel. Amazing.

Setting up Firebase #

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

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 #

Update 2020: I've switched my microservice to an API route inside my Next.js application. If you're using Next.js, I would recommend this approach.

This blog post was inspired by Guillermo Rauch's blog, where he open-sourced his code. I've updated some things 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 ( ) ;

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 ( 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' ) ; } 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.

We can confirm it was logged correctly by checking Firebase.

Deployment with Vercel #

We can configure our microservice to deploy with Vercel through a vercel.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 Vercel CLI, you can deploy in one command 🚀

$ vc --prod

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 ) { if ( ! / already exists / . test ( error . message ) ) { console . error ( 'Firebase initialization error' , error . stack ) ; } } return firebase . database ( ) . ref ( 'views' ) ; }

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 ( '' ) ; 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 ] ) ; 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.

For styling, I'm using styled-components.

For formatting, I'm using comma-number.

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.9 em ; text - transform : uppercase ; letter - spacing : 0.04 em ; $ { ( props ) => props . highlight && css` animation - name : $ { highlightBackgound } ; animation - duration : 1 s ; ` } ` ;

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.

It works! 🎉

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

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.