React Router Integration
This document explains how we integrate Google AdSense with React Router v7, including the challenges of Single Page Applications (SPAs) and how we solve them.
Generated by Opus 4.5 Reviewed by ali
The SPA Challenge
Traditional websites fully reload on each navigation. React Router SPAs only remount components:
The Core Problem
When a React component remounts:
- A new
useRefis created (starts asfalse) - But the
<ins>element may still have an ad from before - Calling
push({})again throws an error
Our Solution
We check if the <ins> element already has an ad by looking at the data-ad-status attribute:
Implementation
useEffect(() => {
// Only initialize if we're rendering a real ad
if (!shouldRenderAd) return
// Check if this specific <ins> element already has an ad loaded
// AdSense sets data-ad-status attribute after processing
if (adRef.current?.dataset?.adStatus) {
return // Already processed, skip
}
try {
(window.adsbygoogle = window.adsbygoogle || []).push({})
} catch (error) {
console.error('AdSense initialization error:', error)
}
}, [shouldRenderAd])
Architecture Overview
Component Hierarchy
root.jsx
├── <head>
│ ├── GoogleTagManagerScript
│ └── GoogleAdSenseScript ← Loads adsbygoogle.js (production only)
│
└── <body>
└── <Outlet /> ← React Router renders routes here
└── home.jsx
├── AdSenseUnit ← HOME_HERO (responsive banner)
└── AdSenseUnit ← HOME_AFTER_HERO (in-article)
Script Loading Strategy
GoogleAdSenseScript Component
Located in app/third-parties/google-ad-sense.jsx:
const ADSENSE_PUBLISHER_ID = import.meta.env.VITE_ADSENSE_PUBLISHER_ID
export function GoogleAdSenseScript() {
// Only render in production (when publisher ID is set)
if (!ADSENSE_PUBLISHER_ID) return null
return (
<script
async
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${ADSENSE_PUBLISHER_ID}`}
crossOrigin="anonymous"
/>
)
}
Why this approach?
- Script only loads when
VITE_ADSENSE_PUBLISHER_IDis set (production) - Uses
asyncattribute so it doesn't block page rendering - Placed in
<head>so it's available when ad components mount
Environment-Based Rendering
Why Environment Gating?
- No test environment: AdSense doesn't provide a sandbox
- Policy compliance: Clicking your own ads violates AdSense policies
- Developer experience: Placeholders show where ads will appear
- Performance: No unnecessary network requests in development
Handling Unfilled Ads
When AdSense can't fill an ad slot, we hide it using CSS:
CSS Implementation
// ad-sense-unit.scss
.ad-sense-wrapper:has(ins.adsbygoogle[data-ad-status="unfilled"]),
.ad-sense-wrapper:has(ins.adsbygoogle[data-ad-status="unfill-optimized"]) {
display: none !important;
}
Why CSS instead of JavaScript?
- No React state management needed
- No MutationObserver complexity
- Instant response (browser-native)
- Works even if JavaScript fails
Responsive Ad Sizing
For banner ads that need specific dimensions per screen size:
Note: The 728x90 leaderboard only appears at 1400px+ because at smaller viewports, Bootstrap columns (like Col lg={7}) are too narrow and would cause overflow.
Implementation
When responsiveClass is provided to AdSenseUnit:
- The CSS class controls dimensions via media queries
data-ad-formatis NOT added (per Google's requirements)- The
<ins>element gets the CSS class applied
// Usage
<AdSenseUnit
slot={AD_SENSE_UNITS.HOME_HERO}
responsiveClass="ad-sense-horizontal-banner"
/>
// ad-sense-unit.scss
.ad-sense-horizontal-banner {
width: 320px;
height: 50px;
max-width: 100%; // Prevent overflow
margin: 0 auto;
@media (min-width: 576px) {
width: 468px;
height: 60px;
}
// Only show leaderboard at xxl where column is wide enough
@media (min-width: 1400px) {
width: 728px;
height: 90px;
}
}
Key Implementation Details
1. Ref-based DOM Access
We use useRef to access the actual DOM element:
const adRef = useRef(null)
// Later, check if element was already processed
if (adRef.current?.dataset?.adStatus) {
return // Skip initialization
}
2. Conditional data-ad-format
// Only add data-ad-format when NOT using responsive class
{ ...(!useResponsiveClass && { 'data-ad-format': format }) }
3. Cleanup Not Required
Unlike many React integrations, we don't need cleanup in useEffect:
- AdSense manages its own lifecycle
- Ads persist across navigations (which is what we want)
- The
data-ad-statuscheck prevents re-initialization
Testing Checklist
Before deploying AdSense changes:
- Placeholders display correctly in development
- No console errors about "already have ads"
- Ads load in production environment
- Navigation away and back doesn't cause errors
- Unfilled ads collapse (no blank space)
- Responsive sizing works on mobile/tablet/desktop