How to Implement Adsterra Ads in React.js / Next.js Projects

Implementing Multiple Adsterra Ads in React.js / Next.js Projects
Integrating Adsterra banner ads into React or Next.js applications is notoriously challenging. Unlike WordPress or static HTML sites where you simply paste ad code into widgets, React’s component-based architecture and client-side rendering create unique complications.
This guide documents a real-world implementation for Ultimate Calc Hub, a calculator website built with React, where we successfully implemented 5 different Adsterra banner ads on the same page after trying multiple approaches.
What We’ll Cover
- Why standard Adsterra integration fails in React
- 5 different approaches we tried (and why 4 failed)
- The final working solution with complete code
- Best practices for production
The Problem with Third-Party Ad Scripts in React
How Adsterra Ads Work
Adsterra provides banner ad codes that look like this:
<script type="text/javascript">
atOptions = {
'key': 'YOUR_AD_KEY_HERE',
'format': 'iframe',
'height': 50,
'width': 320,
'params': {}
};
</script>
<script type="text/javascript" src="//www.highperformanceformat.com/YOUR_AD_KEY/invoke.js"></script>
The fundamental issue: All Adsterra ads use the same global variable name atOptions. When you try to load multiple ads:
- First ad sets
window.atOptions = { key: 'ad1' } - Second ad overwrites it:
window.atOptions = { key: 'ad2' } - Third ad overwrites again:
window.atOptions = { key: 'ad3' } - Result: Only the last ad loads correctly
WordPress vs React
Why it works in WordPress:
- WordPress renders server-side HTML
- Each widget is isolated in the DOM when page loads
- Scripts execute in sequence before any overwriting occurs
Why it fails in React:
- Components mount nearly simultaneously
- React re-renders dynamically
- All components share the same global
windowobject - Race conditions and timing issues
Failed Approaches and Why They Don’t Work
We tried 5 different approaches before finding the solution. Here’s what failed and why:
❌ Approach 1: Direct Script Injection
What we tried:
useEffect(() => {
(window as any).atOptions = {
key: 'ad_key_here',
format: 'iframe',
height: 50,
width: 320,
params: {},
};
const script = document.createElement('script');
script.src = '//www.highperformanceformat.com/ad_key/invoke.js';
document.body.appendChild(script);
}, []);
Why it failed:
- All components execute
useEffectnearly simultaneously - Each overwrites the global
atOptions - Only the last component’s ad loads
- No true isolation between ads
Result: Only 1 ad displays (usually the last one to mount)
❌ Approach 2: Staggered Loading with Delays
What we tried:
const delays = {
'top': 100,
'sidebar-left': 300,
'sidebar-right': 500,
'middle': 700,
'bottom': 900,
};
setTimeout(() => {
(window as any).atOptions = config;
const script = document.createElement('script');
script.src = `//www.highperformanceformat.com/${config.key}/invoke.js`;
container.appendChild(script);
}, delays[position]);
Why it failed:
- Timing is inconsistent across devices
- Network speed affects script loading order
- On slow connections, delays don’t prevent conflicts
- Fast connections might still cause race conditions
- Not sustainable – requires constant tweaking
Result: Sometimes 2-3 ads load, but inconsistent and unreliable
❌ Approach 3: Shared Configuration Object
What we tried:
const adConfigs = {
top: { key: 'ad1', width: 320, height: 50 },
sidebar: { key: 'ad2', width: 160, height: 600 },
};
const AdPlaceholder = ({ position }) => {
const config = adConfigs[position];
// Load ad using config
};
Why it failed:
- Still uses the same global
atOptionsvariable - Shared state doesn’t solve the fundamental conflict
- All components still overwrite each other’s configuration
Result: Same problem – only 1 ad displays
❌ Approach 4: HTML String Parsing
What we tried:
const adHTML = `
<script>atOptions = {...}</script>
<script src="invoke.js"></script>
`;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = adHTML;
Array.from(tempDiv.children).forEach((element) => {
// Clone and append scripts
containerRef.current?.appendChild(element.cloneNode(true));
});
Why it failed:
- Scripts still execute in the main window context
- No isolation from the global scope
- Same
atOptionsconflict persists
Result: Slightly better but still unreliable (1-2 ads max)
❌ Approach 5: Separate Component Files
What we tried: Creating individual files for each ad:
/ads
├── TopAd.tsx
├── SidebarAd.tsx
├── MiddleAd.tsx
└── etc...
Why it failed:
- File separation doesn’t create scope isolation
- All components still access the same global
window - Module boundaries don’t prevent variable conflicts
Result: Same issues – global variable conflicts remain
The Working Solution: iframe srcdoc
Why iframes Work
The only reliable solution is using iframes with the srcdoc attribute. Here’s why:
✅ Complete Isolation: Each iframe has its own window object
✅ No Global Conflicts: atOptions in iframe A ≠ atOptions in iframe B
✅ No Timing Issues: All ads can load simultaneously
✅ Sustainable: No delays, no race conditions, no tweaking needed
✅ WordPress-like Behavior: This is essentially how WordPress isolates widgets
How It Works
const adHTML = `
<!DOCTYPE html>
<html>
<head>
<style>body { margin: 0; padding: 0; }</style>
</head>
<body>
<script type="text/javascript">
atOptions = { 'key': 'YOUR_KEY', ... };
</script>
<script src="//www.highperformanceformat.com/YOUR_KEY/invoke.js"></script>
</body>
</html>
`;
<iframe srcDoc={adHTML} width="320" height="50" />
Each iframe creates a completely isolated JavaScript environment:
- Iframe 1:
iframe1.window.atOptions - Iframe 2:
iframe2.window.atOptions - Main page:
window.atOptions(unused)
No conflicts possible!

Step-by-Step Implementation
Project Structure
For Ultimate Calc Hub, we organized ads like this:
src/
├── components/
│ ├── Layout.tsx
│ ├── Navigation.tsx
│ ├── Footer.tsx
│ └── ads/
│ ├── TopAd.tsx (320x50 banner)
│ ├── SidebarAd.tsx (160x600 skyscraper)
│ ├── MiddleAd.tsx (300x250 medium rectangle)
│ └── BottomAd.tsx (468x60 banner)
Step 1: Get Your Adsterra Ad Codes
- Log into your Adsterra account
- Create separate ad placements for each position
- IMPORTANT: Do NOT reuse the same ad code for multiple positions
- Note down each ad’s key, width, and height
Example ad codes we used:
- Top:
7f2c91a0d54b48cfa3e0b19e82c4f7d9(320×50) - Sidebar:
e3a5c97f12bd48d0aa48f9136d0b72ce(160×600) - Middle:
b8f37c29e14a4d60bc28e0ac519873f4(300×250) - Bottom:
d2b06f8ac1494dea9c1b7368ef42a0b5(468×60)
Step 2: Create Individual Ad Components
Each ad gets its own component file with the iframe approach.
Complete Code Examples
TopAd.tsx (320×50 Banner)
import { useState } from 'react';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
interface TopAdProps {
className?: string;
}
const TopAd = ({ className = '' }: TopAdProps) => {
const [isVisible, setIsVisible] = useState(true);
if (!isVisible) return null;
const adHTML = `
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; overflow: hidden; }
</style>
</head>
<body>
<script type="text/javascript">
atOptions = {
'key': '7f2c91a0d54b48cfa3e0b19e82c4f7d9',
'format': 'iframe',
'height': 50,
'width': 320,
'params': {}
};
</script>
<script type="text/javascript" src="//www.highperformanceformat.com/7f2c91a0d54b48cfa3e0b19e82c4f7d9/invoke.js"></script>
</body>
</html>
`;
return (
<Card
className={`relative border-primary/20 bg-gradient-to-r from-primary/5 to-secondary/5 ${className}`}
style={{ minWidth: '320px', minHeight: '50px' }}
>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 h-6 w-6 p-0 text-muted-foreground hover:text-foreground z-10"
onClick={() => setIsVisible(false)}
>
<X className="h-3 w-3" />
</Button>
<div className="flex items-center justify-center w-full h-full p-2" style={{ minWidth: '320px', minHeight: '50px' }}>
<iframe
srcDoc={adHTML}
style={{
width: '320px',
height: '50px',
border: 'none',
overflow: 'hidden'
}}
scrolling="no"
frameBorder="0"
title="Top Ad"
/>
</div>
</Card>
);
};
export default TopAd;
SidebarAd.tsx (160×600 Skyscraper)
import { useState } from 'react';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
interface SidebarLeftAdProps {
className?: string;
}
const SidebarLeftAd = ({ className = '' }: SidebarLeftAdProps) => {
const [isVisible, setIsVisible] = useState(true);
if (!isVisible) return null;
const adHTML = `
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; overflow: hidden; }
</style>
</head>
<body>
<script type="text/javascript">
atOptions = {
'key': 'e3a5c97f12bd48d0aa48f9136d0b72ce',
'format': 'iframe',
'height': 600,
'width': 160,
'params': {}
};
</script>
<script type="text/javascript" src="//www.highperformanceformat.com/e3a5c97f12bd48d0aa48f9136d0b72ce/invoke.js"></script>
</body>
</html>
`;
return (
<Card
className={`relative border-primary/20 bg-gradient-to-r from-primary/5 to-secondary/5 ${className}`}
style={{ minWidth: '160px', minHeight: '600px' }}
>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 h-6 w-6 p-0 text-muted-foreground hover:text-foreground z-10"
onClick={() => setIsVisible(false)}
>
<X className="h-3 w-3" />
</Button>
<div className="flex items-center justify-center w-full h-full p-2" style={{ minWidth: '160px', minHeight: '600px' }}>
<iframe
srcDoc={adHTML}
style={{
width: '160px',
height: '600px',
border: 'none',
overflow: 'hidden'
}}
scrolling="no"
frameBorder="0"
title="Sidebar Left Ad"
/>
</div>
</Card>
);
};
export default SidebarLeftAd;
Layout.tsx Integration
Here’s how we integrated the ads into Ultimate Calc Hub’s layout:
import { ReactNode, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import Navigation from './Navigation';
import Footer from './Footer';
import TopAd from './ads/TopAd';
import SidebarAd from './ads/SidebarAd';
interface LayoutProps {
children: ReactNode;
showTopAd?: boolean;
}
const Layout = ({ children, showTopAd = true }: LayoutProps) => {
const location = useLocation();
// Google Analytics tracking
useEffect(() => {
if (window.gtag) {
window.gtag('config', 'G-BDFTREXX', {
page_path: location.pathname + location.search,
});
}
}, [location]);
// Show sidebar ads on /tools/* and /blog/* routes
const isAdsPage =
location.pathname.startsWith('/tools/') ||
location.pathname.startsWith('/blog/');
return (
<div className="min-h-screen flex flex-col bg-background">
<Navigation />
{isAdsPage ? (
// Layout with sidebar ads
<div className="flex flex-1 w-full">
{/* Left Sidebar Ad */}
<aside className="hidden lg:block w-80 shrink-0 px-4 pt-4 sticky top-24 self-start h-fit">
<SidebarAd />
</aside>
{/* Main Content with Top Ad */}
<main className="flex-1 px-2 py-4 min-w-0">
{showTopAd && (
<div className="max-w-4xl mx-auto mb-4">
<TopAd />
</div>
)}
<div className="max-w-4xl mx-auto overflow-x-hidden">
{children}
</div>
</main>
{/* Right Sidebar Ad */}
<aside className="hidden lg:block w-80 shrink-0 px-4 pt-4 sticky top-24 self-start h-fit">
<SidebarAd />
</aside>
</div>
) : (
// Default layout without sidebar ads
<>
{showTopAd && (
<div className="container mx-auto px-4 py-4">
<div className="max-w-4xl mx-auto">
<TopAd />
</div>
</div>
)}
<main className="flex-1">{children}</main>
</>
)}
<Footer />
</div>
);
};
export default Layout;
Using Middle and Bottom Ads in Tool Pages
import Layout from '@/components/Layout';
import MiddleAd from '@/components/ads/MiddleAd';
import BottomAd from '@/components/ads/BottomAd';
const ReactionKineticsCalculator = () => {
return (
<Layout>
<h1>Reaction Kinetics Calculator</h1>
{/* Calculator content */}
<div className="calculator-section">
{/* Your calculator UI */}
</div>
{/* Middle Ad - appears between sections */}
<MiddleAd className="my-12" />
{/* More content */}
<div className="information-section">
{/* Educational content */}
</div>
{/* Bottom Ad - appears at the end */}
<BottomAd />
</Layout>
);
};
export default ReactionKineticsCalculator;
Key Features of Our Implementation
1. Close Button for Better UX
Each ad has a close button that lets users dismiss it:
const [isVisible, setIsVisible] = useState(true);
<Button onClick={() => setIsVisible(false)}>
<X className="h-3 w-3" />
</Button>
2. Responsive Design
Ads are hidden on mobile to improve user experience:
<aside className="hidden lg:block">
<SidebarLeftAd />
</aside>
3. Sticky Positioning
Sidebar ads stay visible while scrolling:
position: sticky;
top: 24px;
4. Conditional Ad Display
Show ads only on specific routes:
const isAdsPage =
location.pathname.startsWith('/tools/') ||
location.pathname.startsWith('/blog/');
Troubleshooting
Issue: Ads Not Showing Up
Possible causes:
- Ad blocker enabled – Test in incognito mode
- Adsterra has no inventory – Normal, ads won’t show 100% of the time
- Wrong ad key – Double-check your Adsterra dashboard
- Domain not approved – Ensure your domain is verified in Adsterra
Solution:
// Add error handling
<iframe
srcDoc={adHTML}
onError={() => console.error('Ad failed to load')}
onLoad={() => console.log('Ad loaded successfully')}
/>
Issue: Only Some Ads Load
Cause: Adsterra doesn’t always have inventory for all placements simultaneously.
Solution: This is normal behavior. Ad networks don’t guarantee 100% fill rate. If you consistently see zero ads, contact Adsterra support.
Issue: Ads Showing Wrong Size
Cause: iframe dimensions don’t match Adsterra ad size.
Solution: Ensure iframe size exactly matches ad dimensions:
style={{
width: '320px', // Match Adsterra width
height: '50px', // Match Adsterra height
}}
Issue: Ads Blocked by CSP (Content Security Policy)
Cause: Your server’s CSP headers block third-party scripts.
Solution: Add Adsterra domains to your CSP:
Content-Security-Policy:
script-src 'self' 'unsafe-inline' *.highperformanceformat.com;
frame-src 'self' *.highperformanceformat.com;
Best Practices
1. Use Different Ad Codes
❌ Don’t: Reuse the same ad code for multiple positions. However you should notice i use same MiddleAd componnent for several locations in blog post and it works without any issue at all. With the “iframe srcdoc” method reuse is possible in the smart way i have decsribed in my codes.
// BAD - Don't do this!
const adKey = 'same_key_everywhere';
✅ Do: Create separate placements in Adsterra dashboard
// GOOD - Each position has unique code
top: '7f2c91a0d54b48cfa3e0b19e82c4f7d9'
sidebar: '4c91de72b5084f7db39f1a2697ac3be1'
2. Optimize for Mobile
Hide large ads on mobile devices:
<aside className="hidden lg:block">
<SidebarLeftAd />
</aside>
3. Strategic Ad Placement
- Top ad: First thing users see
- Sidebar ads: Non-intrusive, always visible
- Middle ad: Natural break in content
- Bottom ad: After user engagement
4. Monitor Performance
Track ad metrics in Adsterra dashboard:
- Impressions
- Click-through rate (CTR)
- Revenue per thousand impressions (RPM)
5. Test Thoroughly
Test on:
- ✅ Different browsers (Chrome, Firefox, Safari)
- ✅ Different devices (desktop, tablet, mobile)
- ✅ With and without ad blockers
- ✅ Slow and fast internet connections
Performance Considerations
Bundle Size
Each ad component is small (~2KB). Total overhead for 5 ads: ~10KB.
Loading Performance
Iframes add minimal overhead:
- Initial HTML: ~500 bytes per ad
- Adsterra scripts load asynchronously
- No blocking of main thread
SEO Impact
Ads in iframes don’t affect SEO:
- Content outside iframes is crawlable
- No duplicate content issues
- No page speed penalty (async loading)
Alternative Ad Formats
If you need more ads on the same page, consider mixing formats:
Popunder Ads
// Add to index.html or App.tsx
<script>
var ad_idzone = "YOUR_POPUNDER_ID";
</script>
<script src="//ads.adsterra.com/pop.js"></script>
Social Bar
Non-intrusive sticky bar at bottom of page.
Native Ads
Blend with your content style.
Advantage: Different formats don’t conflict with banner ads!
Comparison: Our Solution vs Other Methods
| Method | Reliability | Performance | Maintenance | Verdict |
|---|---|---|---|---|
| Direct injection | ❌ Poor | ⚡ Fast | ✅ Easy | Don’t use |
| Staggered delays | ⚠️ Inconsistent | ⚡ Fast | ❌ Hard | Don’t use |
| HTML parsing | ⚠️ Unreliable | ⚡ Fast | ⚠️ Medium | Don’t use |
| Separate files | ❌ Poor | ⚡ Fast | ✅ Easy | Don’t use |
| iframe srcdoc | ✅ Excellent | ⚡ Fast | ✅ Easy | ✅ USE THIS |
Conclusion: Implement Adsterra Ads in React.js
After extensive testing and 5 different approaches, iframe srcdoc is the only reliable, sustainable solution for implementing multiple Adsterra ads in React/Next.js applications.
Key Takeaways
✅ Use iframes with srcdoc for complete isolation
✅ Create separate ad placements in Adsterra
✅ Never reuse ad codes across multiple positions
✅ Avoid delay-based solutions – they’re unreliable
✅ Test thoroughly across devices and browsers
Why This Works
The iframe approach mimics how WordPress handles widgets – complete JavaScript isolation. Each ad runs in its own sandboxed environment with its own window.atOptions, eliminating all conflicts.
Real-World Results
Ultimate Calc Hub now successfully displays:
- 5 different Adsterra ads simultaneously
- 100% reliability across all pages
- Zero conflicts or race conditions
- Excellent user experience with close buttons
- Responsive design that works on all devices
Additional Resources
- Adsterra Documentation
- React iframe Documentation
- Next.js Third-Party Scripts
- Ultimate Calc Hub – Live implementation
Questions or Issues?
If you encounter problems:
- Check Adsterra dashboard – Verify ad codes are active
- Test without ad blocker – Disable extensions
- Contact Adsterra support – They can verify your setup
- Review this guide – Ensure exact implementation
Common mistake: Using the same ad code for multiple positions. Create separate placements!
Author’s Note: This guide is based on real implementation challenges faced while building Ultimate Calc Hub. After trying 5 different approaches over several hours, the iframe solution proved to be the only reliable method. I hope this saves you time and frustration!






