Skip to main content

Refreshing AdSense Ads in Single Page Applications (React, Vue)

 Integrating Google AdSense into a Single Page Application (SPA) presents a fundamental architectural conflict. Traditional AdSense implementations assume a multi-page architecture where every navigation event triggers a full page reload, naturally requesting new ad inventory.

In an SPA Google AdSense implementation, client-side routing updates the DOM without a hard reload. The result is a stagnant ad unit that persists across page views. This degrades revenue by failing to generate new impressions, and attempting to manually force ad refreshes incorrectly often leads to AdSense policy violations.

This guide provides a technically rigorous solution for safely handling the adsbygoogle.push route change mechanism in modern React and Vue applications.

The Root Cause of Stale AdSense Units

Understanding the failure requires examining the AdSense execution lifecycle.

When you embed the <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script> snippet, it loads the core AdSense library globally. The library exposes a queue array: window.adsbygoogle.

When a component renders an <ins> tag with the adsbygoogle class and calls (window.adsbygoogle = window.adsbygoogle || []).push({}), the script:

  1. Scans the DOM for unpopulated <ins class="adsbygoogle"> elements.
  2. Injects an <iframe> containing the ad payload into the <ins> tag.
  3. Adds a data-adsbygoogle-status="done" attribute to the <ins> tag.

When a user navigates to a new view via client-side routing, the SPA router swaps out the page components. If the ad component remains in the layout (e.g., in a sidebar) and is not explicitly unmounted, the DOM node remains mutated with the status="done" flag. Calling .push({}) again on a populated tag will throw the error: All ins elements in the DOM with class=adsbygoogle already have ads in them.

To safely request new inventory, the application must destroy the mutated DOM node, render a pristine <ins> tag, and execute a fresh push command.

The Solution: Forcing Component Remounts

The most reliable, policy-compliant method for refreshing ads in an SPA is to leverage the virtual DOM's reconciliation process. By tying the ad component's identity to the current route path, the framework will forcibly unmount the old ad and mount a clean instance on every navigation event.

Implementation for React (AdSense React Router)

In React, the key prop dictates component identity. When the key changes, React destroys the underlying DOM node and builds a new one.

First, define the window.adsbygoogle interface to ensure TypeScript safety.

// global.d.ts
interface Window {
  adsbygoogle: any[];
}

Next, create an isolated AdBanner component. The useEffect hook handles the push execution immediately after the component mounts.

// AdBanner.tsx
import { useEffect } from 'react';

interface AdBannerProps {
  adClient: string;
  adSlot: string;
  format?: string;
  responsive?: string;
}

export const AdBanner = ({
  adClient,
  adSlot,
  format = 'auto',
  responsive = 'true',
}: AdBannerProps) => {
  useEffect(() => {
    try {
      // Execute the push command inside a try/catch to gracefully handle ad blockers
      (window.adsbygoogle = window.adsbygoogle || []).push({});
    } catch (error) {
      console.warn('AdSense injection failed. This is typical if an Ad Blocker is active.', error);
    }
  }, []); // Empty dependency array ensures this runs exactly once per mount

  return (
    <ins
      className="adsbygoogle"
      style={{ display: 'block', minHeight: '250px' }} // minHeight prevents Layout Shift (CLS)
      data-ad-client={adClient}
      data-ad-slot={adSlot}
      data-ad-format={format}
      data-full-width-responsive={responsive}
    />
  );
};

To trigger the refresh, pass the current route path from React Router into the key prop of the AdBanner.

// PageLayout.tsx
import { useLocation } from 'react-router-dom';
import { AdBanner } from './AdBanner';

export const PageLayout = ({ children }: { children: React.ReactNode }) => {
  const location = useLocation();

  return (
    <div className="layout-container">
      <main className="content">{children}</main>
      
      <aside className="sidebar">
        {/* The key prop forces unmount/remount on route changes */}
        <AdBanner 
          key={location.pathname} 
          adClient="ca-pub-XXXXXXXXXXXXXXXX" 
          adSlot="1234567890" 
        />
      </aside>
    </div>
  );
};

Implementation for Vue (Refresh AdSense Vue)

The architectural concept remains identical in Vue 3. We utilize the Composition API and tie the component lifecycle to the Vue Router state.

Create the reusable Vue component:

<!-- AdBanner.vue -->
<template>
  <ins
    class="adsbygoogle"
    style="display: block; min-height: 250px;"
    :data-ad-client="adClient"
    :data-ad-slot="adSlot"
    :data-ad-format="format"
    :data-full-width-responsive="responsive"
  ></ins>
</template>

<script setup lang="ts">
import { onMounted } from 'vue';

defineProps({
  adClient: { type: String, required: true },
  adSlot: { type: String, required: true },
  format: { type: String, default: 'auto' },
  responsive: { type: String, default: 'true' }
});

onMounted(() => {
  try {
    // @ts-ignore: Bypass TS check for dynamically injected global variable
    (window.adsbygoogle = window.adsbygoogle || []).push({});
  } catch (error) {
    console.warn('AdSense injection failed.', error);
  }
});
</script>

In your layout or parent view, bind the Vue Router fullPath to the :key attribute of the component.

<!-- DefaultLayout.vue -->
<template>
  <div class="layout-container">
    <main class="content">
      <router-view />
    </main>

    <aside class="sidebar">
      <!-- Changing the key forces the component to destroy and recreate -->
      <AdBanner 
        :key="route.fullPath" 
        adClient="ca-pub-XXXXXXXXXXXXXXXX" 
        adSlot="1234567890" 
      />
    </aside>
  </div>
</template>

<script setup lang="ts">
import { useRoute } from 'vue-router';
import AdBanner from './AdBanner.vue';

const route = useRoute();
</script>

Deep Dive: Why This Implementation is Safer

Manually attempting to clear the innerHTML of the <ins> tag and re-calling .push({}) is an anti-pattern. Google's ad scripts attach event listeners and perform deep DOM mutations within that container. Stripping the inner HTML leaves orphaned event listeners and causes memory leaks.

By utilizing the framework's native diffing engine (key prop), you delegate the cleanup to React/Vue. The framework unmounts the element, allowing the JavaScript garbage collector to clear the memory footprint. When the new route resolves, a functionally new <ins> element is painted to the DOM, creating a standard environment for the adsbygoogle script.

Common Pitfalls and Edge Cases

Cumulative Layout Shift (CLS)

Ad units load asynchronously. If an <ins> tag is rendered with no intrinsic dimensions, the layout will collapse. When the iframe eventually injects, the layout violently expands, penalizing your Core Web Vitals score. Always apply a CSS min-height and min-width to the <ins> tag or its direct wrapper that matches your ad format.

AdSense Policy Violations

Google's ad placement policies strictly prohibit artificially inflating impressions. Never use setInterval or setTimeout to trigger the push command or remount the component. Ads must only be refreshed as a direct result of user interaction (e.g., clicking a link that triggers client-side routing to a new view). The routing method demonstrated above complies with these policies because an actual page transition is occurring from the user's perspective.

Handling Ad Blockers

If a user runs an ad blocker, window.adsbygoogle.push({}) will fail because the network request for adsbygoogle.js was intercepted, meaning the array prototype lacks the specific push interception methods Google applies. Uncaught errors in a useEffect or onMounted hook can interrupt component lifecycles in React/Vue. Always wrap the execution in a try/catch block.

Local Development Blank Spaces

Google AdSense relies on domain verification. When testing locally (localhost or 127.0.0.1), Google often refuses to fill the ad slot, resulting in a blank space. This is expected behavior. To verify your implementation is functioning, check the browser console; if no AdSense errors are thrown upon navigation, the infrastructure is sound and will populate correctly in a production environment.