Single Page Applications
This guide covers how to integrate Moli with single page applications (SPAs) built with frameworks like React, Vue, Angular, or vanilla JavaScript. SPAs require special handling because they don't trigger full page reloads when users navigate.
Overview
SPAs present unique challenges for ad tags:
- No page reloads mean ads don't automatically refresh
- Route changes need to trigger new ad requests
- Ad slots may be dynamically created and destroyed
- State management becomes more complex
Moli provides built-in SPA support to handle these challenges.
Basic SPA Setup
A lot of the examples are generated with AI and have not been tested in production. Take extra care in handling state management and check that ad slots are rendered only once.
1. Enable SPA Mode
Configure Moli for SPA mode in your configuration:
const moliConfig = {
// ... other configuration
spa: {
enabled: true,
validateLocation: 'href' // or 'pathname' or 'none'
}
};
2. Initialize Moli
Set up Moli with SPA support:
// Initialize Moli
window.moli = window.moli || { que: [] };
window.moli.que.push(function(moliAdTag) {
// Initial ad request
moliAdTag.requestAds();
});
3. Handle Route Changes
Trigger new ad requests when routes change:
// Example with React Router
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
useEffect(() => {
// Request ads on route change
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
}, [location]);
return (
<div>
{/* Your app content */}
</div>
);
}
Route Change Detection
validateLocation Options
The validateLocation setting determines what constitutes a route change:
const moliConfig = {
spa: {
enabled: true,
validateLocation: 'href' // Options: 'href', 'pathname', 'none'
}
};
'href'(default) - Full URL changes trigger new ad requests- `'pathname' - Only path changes trigger new ad requests (ignores query params and hash)
- `'none' - No automatic detection (manual control only)
Manual Route Change Handling
For frameworks without built-in route change detection:
// Custom route change handler
function handleRouteChange(newUrl) {
// Update browser history
window.history.pushState({}, '', newUrl);
// Trigger ad request
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
}
// Listen for browser back/forward
window.addEventListener('popstate', () => {
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
});
Framework-Specific Integration
React
Basic React Integration
import React, { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
useEffect(() => {
// Initialize Moli on first load
if (!window.moli) {
window.moli = { que: [] };
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
} else {
// Request ads on route change
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
}
}, [location]);
return (
<div>
<Header />
<main>
<Routes />
</main>
<Footer />
</div>
);
}
Ad Slot Component
This is a minimal example of an AdSlot component in React.
import React, { useEffect, useRef } from 'react';
interface AdSlotProps {
id: string;
className?: string;
style?: React.CSSProperties;
}
const AdSlot: React.FC<AdSlotProps> = ({ id, className, style }) => {
const adRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Refresh ad slot when component mounts
window.moli.que.push(function(moliAdTag) {
moliAdTag.refreshAdSlot(id);
});
// Cleanup when component unmounts
return () => {
// Optional: Clean up ad slot. This is usually done by the ad tag itself on requestAds()
if (adRef.current) {
adRef.current.innerHTML = '';
}
};
}, [id]);
return (
<div
ref={adRef}
id={id}
className={className}
style={style}
/>
);
};
export default AdSlot;
Usage in Components
function HomePage() {
return (
<div>
<h1>Welcome to Our Site</h1>
<AdSlot id="header-ad" className="ad-container" />
<main>
<p>Content here...</p>
<AdSlot id="content-ad" className="ad-container" />
<p>More content...</p>
</main>
<AdSlot id="sidebar-ad" className="ad-container" />
</div>
);
}
Vue.js
Vue 3 Composition API
<template>
<div>
<header>
<AdSlot id="header-ad" />
</header>
<main>
<router-view />
</main>
<footer>
<AdSlot id="footer-ad" />
</footer>
</div>
</template>
<script setup>
import { onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import AdSlot from './components/AdSlot.vue';
const route = useRoute();
// Initialize Moli
onMounted(() => {
window.moli = window.moli || { que: [] };
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
});
// Handle route changes
watch(() => route.path, () => {
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
});
</script>
Ad Slot Component
<template>
<div :id="id" :class="className" :style="style"></div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue';
const props = defineProps({
id: {
type: String,
required: true
},
className: {
type: String,
default: ''
},
style: {
type: Object,
default: () => ({})
}
});
onMounted(() => {
window.moli.que.push(function(moliAdTag) {
moliAdTag.refreshAdSlot(props.id);
});
});
onUnmounted(() => {
// Optional cleanup
const element = document.getElementById(props.id);
if (element) {
element.innerHTML = '';
}
});
</script>
Angular
Angular Service
import { Injectable } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class MoliService {
constructor(private router: Router) {
this.initializeMoli();
this.handleRouteChanges();
}
private initializeMoli(): void {
(window as any).moli = (window as any).moli || { que: [] };
(window as any).moli.que.push((moliAdTag: any) => {
moliAdTag.requestAds();
});
}
private handleRouteChanges(): void {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => {
(window as any).moli.que.push((moliAdTag: any) => {
moliAdTag.requestAds();
});
});
}
refreshAdSlot(id: string): void {
(window as any).moli.que.push((moliAdTag: any) => {
moliAdTag.refreshAdSlot(id);
});
}
}
Ad Slot Component
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { MoliService } from '../services/moli.service';
@Component({
selector: 'app-ad-slot',
template: '<div [id]="id" [class]="className" [style]="style"></div>'
})
export class AdSlotComponent implements OnInit, OnDestroy {
@Input() id!: string;
@Input() className: string = '';
@Input() style: any = {};
constructor(private moliService: MoliService) {}
ngOnInit(): void {
this.moliService.refreshAdSlot(this.id);
}
ngOnDestroy(): void {
// Optional cleanup
const element = document.getElementById(this.id);
if (element) {
element.innerHTML = '';
}
}
}
Dynamic Ad Slots
Lazy Loading Ad Slots
Load ad slots only when they're about to be visible:
import React, { useEffect, useRef, useState } from 'react';
const LazyAdSlot = ({ id, className }) => {
const [isVisible, setIsVisible] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !isLoaded) {
setIsVisible(true);
setIsLoaded(true);
// Load the ad
window.moli.que.push(function(moliAdTag) {
moliAdTag.refreshAdSlot(id);
});
}
},
{ threshold: 0.1 }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [id, isLoaded]);
return (
<div ref={ref} className={className}>
{isVisible && <div id={id} />}
{!isVisible && (
<div className="ad-placeholder">
<div className="loading-spinner" />
</div>
)}
</div>
);
};
Conditional Ad Slots
Show/hide ad slots based on conditions:
const ConditionalAdSlot = ({ id, show, className }) => {
useEffect(() => {
if (show) {
window.moli.que.push(function(moliAdTag) {
moliAdTag.refreshAdSlot(id);
});
}
}, [show, id]);
if (!show) return null;
return <div id={id} className={className} />;
};
// Usage
<ConditionalAdSlot
id="premium-ad"
show={isPremiumUser()}
className="ad-container"
/>
State Management
Targeting Updates
Update targeting for each route:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
useEffect(() => {
window.moli.que.push(function(moliAdTag) {
// Update targeting for new route
moliAdTag.setTargeting('page_url', location.pathname);
moliAdTag.setTargeting('page_title', document.title);
// Add route-specific targeting
if (location.pathname.includes('/sports/')) {
moliAdTag.setTargeting('section', 'sports');
moliAdTag.addLabel('sports-content');
} else if (location.pathname.includes('/news/')) {
moliAdTag.setTargeting('section', 'news');
moliAdTag.addLabel('news-content');
}
moliAdTag.requestAds();
});
}, [location]);
return <Routes />;
}
User State Management
Handle user state changes:
const useUserState = () => {
const [user, setUser] = useState(null);
useEffect(() => {
// Update targeting when user state changes
window.moli.que.push(function(moliAdTag) {
if (user) {
moliAdTag.setTargeting('user_type', user.type);
moliAdTag.setTargeting('user_id', user.id);
moliAdTag.addLabel('authenticated');
if (user.isPremium) {
moliAdTag.addLabel('premium');
}
} else {
moliAdTag.setTargeting('user_type', 'anonymous');
moliAdTag.addLabel('anonymous');
}
});
}, [user]);
return { user, setUser };
};
Performance Optimization
Debounced Route Changes
Prevent excessive ad requests during rapid navigation:
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
const timeoutRef = useRef(null);
useEffect(() => {
// Clear previous timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Debounce ad requests
timeoutRef.current = setTimeout(() => {
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
}, 100); // 100ms delay
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [location]);
return <Routes />;
}
Ad Slot Caching
Cache ad slots to avoid unnecessary refreshes:
const AdSlotCache = new Set();
const CachedAdSlot = ({ id, className }) => {
useEffect(() => {
if (!AdSlotCache.has(id)) {
window.moli.que.push(function(moliAdTag) {
moliAdTag.refreshAdSlot(id);
});
AdSlotCache.add(id);
}
}, [id]);
return <div id={id} className={className} />;
};
Error Handling
Route Change Errors
Handle errors during route changes:
const handleRouteChange = async () => {
try {
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
} catch (error) {
console.error('Failed to request ads on route change:', error);
// Fallback: retry after delay
setTimeout(() => {
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
}, 1000);
}
};
Ad Loading Errors
Handle ad loading failures:
useEffect(() => {
window.moli.que.push(function(moliAdTag) {
moliAdTag.addEventListener('afterRequestAds', (event) => {
if (event.state === 'error') {
console.error('Ad request failed on route change');
// Implement fallback behavior
}
});
moliAdTag.requestAds();
});
}, [location]);
Testing
Unit Testing
Test SPA integration with Jest:
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
// Mock Moli
const mockMoliAdTag = {
requestAds: jest.fn(),
refreshAdSlot: jest.fn(),
setTargeting: jest.fn(),
addLabel: jest.fn()
};
global.window.moli = {
que: []
};
beforeEach(() => {
global.window.moli.que = [];
jest.clearAllMocks();
});
test('requests ads on route change', () => {
render(
<BrowserRouter>
<App />
</BrowserRouter>
);
// Simulate route change
window.history.pushState({}, '', '/new-route');
window.dispatchEvent(new PopStateEvent('popstate'));
// Check that ads were requested
expect(global.window.moli.que.length).toBeGreaterThan(0);
});
Integration Testing
Test with Cypress:
// cypress/integration/spa.spec.js
describe('SPA Integration', () => {
it('should load ads on route change', () => {
cy.visit('/');
// Check initial ads
cy.get('#header-ad').should('be.visible');
// Navigate to new route
cy.visit('/about');
// Check that ads are refreshed
cy.get('#header-ad').should('be.visible');
// Verify ad requests were made
cy.window().then((win) => {
expect(win.moli.que.length).to.be.greaterThan(0);
});
});
});
Best Practices
1. Initialize Once
Initialize Moli only once at app startup:
// App.tsx
useEffect(() => {
// Only initialize once
if (!window.moli) {
window.moli = { que: [] };
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
}
}, []); // Empty dependency array
2. Clean Up Ad Slots
Clean up ad slots when components unmount:
useEffect(() => {
return () => {
// Clean up ad slot
const element = document.getElementById(id);
if (element) {
element.innerHTML = '';
}
};
}, [id]);
3. Handle Loading States
Show loading states during ad requests:
const [adsLoading, setAdsLoading] = useState(false);
useEffect(() => {
setAdsLoading(true);
window.moli.que.push(function(moliAdTag) {
moliAdTag.addEventListener('afterRequestAds', (event) => {
setAdsLoading(false);
});
moliAdTag.requestAds();
});
}, [location]);
return (
<div>
{adsLoading && <div className="loading-indicator" />}
<AdSlot id="header-ad" />
</div>
);
4. Optimize Performance
Use performance optimizations:
// Debounce rapid route changes
const debouncedRequestAds = useMemo(
() => debounce(() => {
window.moli.que.push(function(moliAdTag) {
moliAdTag.requestAds();
});
}, 100),
[]
);
useEffect(() => {
debouncedRequestAds();
}, [location, debouncedRequestAds]);
Troubleshooting
Common Issues
Ads not refreshing on route change:
- Check that SPA mode is enabled in configuration
- Verify route change detection is working
- Ensure
requestAds()is called on route changes
Duplicate ad requests:
- Check for multiple route change listeners
- Verify debouncing is working correctly
- Ensure cleanup is happening properly
- Use the frequency capping module to add frequency caps on ad slots to have a safe guard for double rendering
Ad slots not appearing:
- Check that ad slot IDs match configuration
- Verify components are mounting correctly
- Check for CSS conflicts
Next Steps
Now that you have SPA integration working, explore these advanced topics: