Using RightMessage with Single Page Applications (SPAs)
If you're building with React, Vue, Angular, or any other SPA framework, you've probably noticed that RightMessage doesn't automatically pick up on your route changes. That's because SPAs don't do full page reloads – they just swap out content dynamically.
Here's how to fix that.
The Problem
Traditional websites trigger a fresh page load every time you navigate. SPAs don't. They update the URL and swap content, but as far as the browser's concerned, you're still on the same "page."
RightMessage needs to know when these transitions happen so it can:
- Re-evaluate personalizations for the new route
- Check if any widgets should display
- Track the "pageview" properly
- Update visitor context
The Solution (It's One Line)
Whenever your route changes, just tell RightMessage to reset:
RM.push(['reset'])
That's it. This tells RightMessage "hey, treat this like a brand new page load."
Now let's look at how to implement this in your framework of choice.
Framework Implementation Guides
Next.js
Pop this into your _app.js
(or _app.tsx
if you're using TypeScript):
// pages/_app.js import { useEffect } from 'react' import { useRouter } from 'next/router' function MyApp({ Component, pageProps }) { const router = useRouter() useEffect(() => { const handleRouteChange = () => { if (window.RM) { window.RM.push(['reset']) } } router.events.on('routeChangeComplete', handleRouteChange) // Cleanup return () => { router.events.off('routeChangeComplete', handleRouteChange) } }, [router]) return <Component {...pageProps} /> }
Vue.js (with Vue Router)
Add this to your router setup:
// router/index.js const router = createRouter({ // your router config }) router.afterEach((to, from) => { // Only reset if we actually changed routes if (to.path !== from.path) { window.RM?.push(['reset']) } })
React Router v6
For the latest React Router, use the useLocation
hook:
import { useEffect } from 'react' import { useLocation } from 'react-router-dom' function App() { const location = useLocation() useEffect(() => { // Skip the initial mount if (window.RM) { window.RM.push(['reset']) } }, [location.pathname]) // Only fire on pathname changes return ( // Your app routes ) }
React Router v5
Got an older React Router? Here are your options:
// Hook approach (functional components) import { useHistory } from 'react-router-dom' function App() { const history = useHistory() useEffect(() => { const unlisten = history.listen(() => { window.RM?.push(['reset']) }) return unlisten }, [history]) } // Class component approach import { withRouter } from 'react-router-dom' class App extends Component { componentDidMount() { this.unlisten = this.props.history.listen(() => { window.RM?.push(['reset']) }) } componentWillUnmount() { this.unlisten() } } export default withRouter(App)
Angular
In your main app component:
// app.component.ts import { Component, OnInit } from '@angular/core' import { Router, NavigationEnd } from '@angular/router' import { filter } from 'rxjs/operators' declare global { interface Window { RM: any } } @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent implements OnInit { constructor(private router: Router) {} ngOnInit() { this.router.events.pipe( filter(event => event instanceof NavigationEnd) ).subscribe((event: NavigationEnd) => { if (window.RM) { window.RM.push(['reset']) } }) } }
Common Gotchas & How to Fix Them
"RM is not defined"
If you've modified how our script is loaded, you might end up dropping the RM
variable we proactively add to the window
object prior to loading our script. Always check for window.RM
before calling it:
if (window.RM) { window.RM.push(['reset']) }
Personalizations Not Updating
Make sure you're calling reset AFTER the route change completes, not before. Most routers have "after" hooks for this reason.
Double Tracking
Some routers fire multiple events per navigation. Add logic to prevent duplicate resets:
let lastPath = window.location.pathname router.on('routeChange', () => { if (window.location.pathname !== lastPath) { lastPath = window.location.pathname window.RM?.push(['reset']) } })
SSR/SSG Considerations
If you're server-side rendering, window
won't exist during the build. Always check:
if (typeof window !== 'undefined' && window.RM) { window.RM.push(['reset']) }
Testing Your Implementation
Want to make sure it's working? Here's a quick test:
- Enable debug mode:
?debug=true
- Navigate between routes in your SPA
- Verify personalizations or Flow changes update on route change
- Check that widgets display based on new URL rules
Advanced Patterns
Debouncing Rapid Navigation
If your app allows super fast navigation, you might want to debounce:
let resetTimeout function debouncedReset() { clearTimeout(resetTimeout) resetTimeout = setTimeout(() => { window.RM?.push(['reset']) }, 100) }
Selective Resets
Maybe you only want to reset on certain routes:
const significantRoutes = ['/products', '/pricing', '/signup'] router.on('routeChange', (url) => { if (significantRoutes.some(route => url.startsWith(route))) { window.RM?.push(['reset']) } })
Custom Data on Route Change
You can also push custom data during resets:
router.on('routeChange', () => { window.RM?.push(['set', { lastRoute: document.referrer, routeChangedAt: new Date().toISOString() }]) window.RM?.push(['reset']) })
Still Having Issues?
First, check the basics:
- Is the RightMessage script loaded on all pages?
- Are you seeing any console errors?
- Is the reset actually being called? (Add a
console.log
to check) - Are your personalization rules URL-based? (They need to be for route changes to matter)
If you're still stuck, hit us up at support@rightmessage.com with:
- Your framework and version
- The implementation code you're using
- What's happening vs. what you expect
- Any console errors
We've helped hundreds of devs get this working, so don't hesitate to reach out. SPAs are tricky, but once you get the reset working, everything else just clicks into place.