How I wrote a ReactJS AdBlock Detection That Didn’t Actually Suck

Don’t like reading? Check out the code.

Background

I began a new contract late last year which operates by setting up a site that tracks clickthroughs to fashion merchants, and attributing purchases at those merchants to our site as the source of the referral. We would then award the user points for that purchase which would accumulate to cash rewards towards future purchases. This apporach required no administrative or technical work on our merchant partners’ end, and with the company’s previous experience in the eCommerce CashBack and Rewards industry, it was very easy to sign up a large number of of partner retailers.

After a quick development cycle, and some testing the platform to confirmed that purchases were properly attributed, we launched on Christmas Day. After the holidays, and analyzing the data, we realized that some of these purchases couldn’t properly be attributed to our users. After a short amount of A/B testing and data collection, we realized that this was due to the fact that some users were running an ad blocker of some sort.

The Problem with AdBlocker

While our site does not (and never will) run ads (because the clickthroughs generate a sufficient amount of revenue), some of our retailer partners must be running scripts or ads that help attribute that referred user’s purchase, and while we can track this clickthrough on our end, the tracking ended there. We couldn’t effectively determine if that user actually ended up making a purchase. Or, if that user left the purchase process (abandoning their cart, effectively), but later picked up again, those tracking methods were no longer in place.

The Solution

It became apparent that we would then need to advise our users that the use of their ad blocker would hinder their ability to collect points on retailer partner purchases. We figured that we would inform the user of this only if an active ad blocker was actually running. We’d describe the situation for the user, and due to the importance of the issue, we would need to effectively force our user to turn off their ad blocker.

A blocking-style AdBlock Detection Notice

The Development Process

It is important to note that the site is build entirely on ReactJS. By using Create-React-App, we were able to build a platform that could then be easily minified for production. Additionally, we have a lean-but-necessary approach to including Node modules.

The Options

As it turns out, there are a couple of Node modules that attempt to detect ad blockers, but don’t seem to be very effective inside of the React environment. There are some very fine and well-developed JavaScript modules, but again, are not effective in a virtual-DOM React environment. It became clear to us that we’d need to spin up our own solution with a React-first approach.

The DetectAdBlock Component

Let’s start with a basic React Component:

import React, { Component } from ‘react’;
class DetectAdBlock extends Component {
render() {
return (
<div id="adblock-wrapper">
</div>
)
}
}
export default DetectAdBlock;

Great. A super basic component. We ended up sticking this into another component of ours, GeneralPageTransitionEvents, which runs on all ReactRouter matches of * and performs various methods on each page load. So, essentially, we will check for an ad blocker with every page. This detail will become really important, later.

We probably want also render some JSX when an ad blocker is detected, so let’s add a method to grab that, and call it in our render method like so.

import React, { Component } from ‘react’;
class DetectAdBlock extends Component {
noticeContentJSX() {
return (
<div id="adblock-notice">
<div className="message">
<h3>Hey, you!</h3>
<p>...</p>
<Button
onClick={this.detectAdBlocker>
>Check for Adblocker again</Button>
</div>
</div>
);
}
    render() {
return (
<div id="adblock-wrapper">
{ this.noticeContentJSX() }
</div>
)
}
}
export default DetectAdBlock;

Now, we have something that, with a little bit of styling (not in the context of this article) would show a notice to the user regarding that an adblocker was detected.

Remember, we only want to show a message to the user if there was an ad blocker detected; otherwise, we probably should show nothing. This means we will need to introduce some React state. Let’s update our DetectAdBlock.js file to this:

import React, { Component } from ‘react’;
class DetectAdBlock extends Component {
constructor(props) {
super(props);
this.state = {
adBlockDetected: false
}
}
    noticeContentJSX() {
return (
<div id="adblock-notice">
<div className="message">
<h3>Hey, you!</h3>
<p>...</p>
<Button
onClick={this.detectAdBlocker>
>Check for Adblocker again</Button>
</div>
</div>
);
}
    render() {
return (
<div id="adblock-wrapper">
{ this.state.adBlockDetected
? this.noticeContentJSX()
: null
}

</div>
)
}
}
export default DetectAdBlock;

Our component now defaults to having no ad blocker detected; Until we change the state of the component so that adBlockDetected is true, however, our notice will never show. Let’s add an empty method that will hold our code for checking for this.

It would be nice to check this as soon as the page loads, so we will also add one of the React lifecycle methods that will call our method, componentDidMount().

import React, { Component } from ‘react’;
class DetectAdBlock extends Component {
constructor(props) {
super(props);
this.state = {
adBlockDetected: false
}
        this.detectAdBlocker = this.detectAdBlocker.bind(this);
}
    componentDidMount() {
this.detectAdBlocker();
}
    detectAdBlocker() {
}
    noticeContentJSX() {
return (
<div id="adblock-notice">
<div className="message">
<h3>Hey, you!</h3>
<p>...</p>
<button
onClick={this.detectAdBlocker
>
Check for Adblocker again
</button>
</div>
</div>
);
}
    render() {
return (
<div id="adblock-wrapper">
{ this.state.adBlockDetected
? this.noticeContentJSX()
: null
}
</div>
)
}
}
export default DetectAdBlock;

So far, so good; when the component mounts, detectAdBlocker will be executed. But, nothing is going on in there. Time to update the method to perform the following:

detectAdBlocker() {
const head = document.getElementsByTagName('head')[0];

const noAdBlockDetected = () => {
this.setState({
adBlockDetected: false
});
}
    const adBlockDetected = () => {
this.setState({
adBlockDetected: true
});
}
    // we will dynamically generate some 'bait'.
const script = document.createElement('script');
script.id = 'adblock-detection';
script.type = 'text/javascript';
script.src = '/ads.js';
script.onload = noAdBlockDetected;
script.onerror = adBlockDetected;
    head.appendChild(script);
}

So, what’s happening here?

Essentially, we grab the document’s head element, and attempt to inject a script suspiciously titled “ads.js.” If this succeeds (onload), we know that it wasn’t blocked. If it fails (onerror), we can reasonably ascertain that it was blocked. We then setState accordingly.

You’ll also want to add ads.js to your public folder at the root, with the following contents:

console.log('i am an ads.js file');

If there is an error in this script, OR if this script does not exist, the onerror method will always fire, and therefore the script will always have errors, and thusly, our page will always think it has an adblocker in place.

Great! We now have the basics of an adblock detection that pretty much covers 90% of the ad blockers out there!

But wait… there’s a problem.

There’s one small problem, however. This only fires once, when the entire React page instance mounts. If the user transitions to another page within the same React App, this method will never execute again, at least until we refresh the page.

This is where other third-party adblock scripts also failed. How can we handle transitions that don’t also cuase the component to remount? Well, there’s another React lifecycle method called componentWillUpdate that executes when the component receives new props or state. Let’s write out that method:

componentWillUpdate(nextProps, nextState) {
this.detectAdBlocker();
}

The good news is, whenever this component updates (which, if you are wrapping your root element in something like Redux, React-Router, or anything that generally updates state/props globally), we’ll be able to detect for ad blockers again.

The bad news is, once we detect an ad blocker, we change our own component’s state. This, in turn, will execute this method again. This will result in a very slow browser (at best), or an eventual maximum call stack exceeded error (at worst). This isn’t good.

I tried several methods to mitigate this. First, I had the script run in a timer every 5 or 10 seconds (invalidating the timer on each componentWillUpdate execution… nothing like a compounding timer to make things really crazy). While the benefits of this approach meant that I could detect, almost immediately, whether the adblocker was turned on or turned off without a page reload, this single benefit did not outweigh the fact that I had a mostly unnecessary script running on a continual basis.

What I eventually settled on was simply checking to see if the page URL had changed. This would occur on the next page load, and that was good enough for us. Here’s the updated componentWillUpdate.

componentWillUpdate(nextProps, nextState) {
if (this.props.route.key !== nextProps.route.key) {
this.detectAdBlocker();
}
}

(this.props.route.key was, for us, specifically formulated in the parent of this component… you could just as well use pass in window.location.pathname into the component at it’s parent:

<DetectAdBlock pathname={window.location.pathname} />

And modify your componentWillUpdate to this:

componentWillUpdate(nextProps, nextState) {
if (this.props.pathname !== nextProps.pathname) {
this.detectAdBlocker();
}
}

Also, we keep injecting a new bait script tag in detectAdBlocker(). We ended up cleaning this up up as well.

I now have a DetectAdBlock component that effectively detects the big players, such as AdBlock, AdBlockPlus and uBlockOrigin. There are still some less-popular adblockers that aren’t yet detected, as well as ad blocking outside of the browser… but this is just a start.

The entire DetectAdBlock.js Component

/**
*
* Detect Ad Blockers
*
* Copyright (c) 2017 James Robert Perih
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import React, { Component, PropTypes } from 'react';
class DetectAdBlock extends Component {
static propTypes = {
pathname: PropTypes.string.isRequired
};
    constructor(props) {
super(props);
        this.state = {
adBlockDetected: false
}
        this.detectAdBlocker = this.detectAdBlocker.bind(this);
}
    componentDidMount() {
this.detectAdBlocker();
}
    componentWillUpdate(nextProps, nextState) {
if (this.props.pathname !== nextProps.pathname) {
this.detectAdBlocker();
}
}
    detectAdBlocker() {
const head = document.getElementsByTagName('head')[0];

const noAdBlockDetected = () => {
this.setState({
adBlockDetected: false
});
}
        const adBlockDetected = () => {
this.setState({
adBlockDetected: true
});
}
        // clean up stale bait
const oldScript =
document.getElementById('adblock-detection');
if (oldScript) {
head.removeChild(oldScript);
}
        // we will dynamically generate some 'bait'.
const script = document.createElement('script');
script.id = 'adblock-detection';
script.type = 'text/javascript';
script.src = '/ads.js';
script.onload = noAdBlockDetected;
script.onerror = adBlockDetected;
        head.appendChild(script);
}
    noticeContentJSX() {
return (
<div id="adblock-notice">
<div className="message">
<h3>Hey, you!</h3>
<p>Your adblocker is on again.</p>
<button
onClick={this.detectAdBlocker}
>
Check for Adblocker again
</button>
</div>
</div>
);
}
    render() {
return (
<div id="adblock-wrapper">
{ this.state.adBlockDetected
? this.noticeContentJSX()
: null
}
</div>
)
}
}
DetectAdBlock.defaultProps = {
pathname: ''
}
export default DetectAdBlock;

Use it like this in component that get’s updated regularly, such as inside your React Router.

import DetectAdBlock from './path/to/DetectAdBlock';
... (the rest of your component) ...
<DetectAdBlock pathname={window.location.pathname} />

I’ll be checking out other open-source adblocker scripts, and adopting some of their methods for detecting ad blockers. I’ve also posted this code on GitHub, and perhaps the community can assist me with improving the results.

And, while even I use an ad blocker on a regular basis, normally I wouldnt want to force a user to disable theirs. However, I was more concerned with the missing purchase attribution, and therefore, rewards and points, that users’ were missing out on. We took this approach, because happy users were more important to us, and felt that they would agree. Again, the site would NEVER run any ads; we just wanted to ensure that users’ received their points.