Single Page Apps
A vs B has built-in support for single-page applications (SPAs). When a visitor moves between pages without a full browser reload, the snippet automatically re-checks experiments for the new URL. There is nothing to switch on.
The problem with SPAs
On a traditional site, every link click triggers a full page load. The browser re-downloads the HTML, re-runs every script including the snippet, and everything starts fresh. The snippet handles this without any trouble.
In a single-page app, clicking a link just swaps the content in place. The URL changes, but the page never reloads, so the snippet doesn't re-run on its own. Without special handling, it would only evaluate experiments on the very first load, and any experiments targeting other URLs would never run.
How A vs B handles SPA navigation
A vs B patches the browser's History API (pushState and replaceState) and
also listens for the popstate event (the back and forward buttons). When any of
these fire, it detects the route change and automatically:
- Removes the variation CSS and JavaScript from the previous page (cleaning up the old experiments).
- Re-runs your Project JavaScript.
- Re-checks every experiment against the new URL and audience rules.
- Applies the right variation code for any experiments that match the new URL.
This is always on. There is no project setting to enable, and no extra code to add: installing the snippet is the whole setup. On a traditional multi-page site the History API is never used, so the handling simply never fires.
Two things it deliberately does not treat as navigation:
- The same URL again. Many apps call
replaceStatefor scroll restoration and other bookkeeping, sometimes several times per page. A vs B compares the URL first and does nothing when it has not changed, so your variations are not torn down and re-applied for no reason. - A changed
#fragment. A link to#featureson the current page is not a new page. Turn on Hash-based routing (below) if your app really does route with hash URLs.
Importantly, a visitor's variation assignments are preserved across navigations. If someone was bucketed into Variant 1, they stay in Variant 1 every time they visit a page where that experiment runs, even after navigating away and back.
Re-checking experiments yourself
Some apps change what the page is without changing the URL: a multi-step form,
a modal route, a locale switch, a virtualised list that swaps its whole contents.
Call avsb.refresh() at that moment and A vs B does exactly what it does on a
route change: tear the active experiments down, re-run your Project JavaScript,
and re-evaluate everything against the page as it is now.
// After your app has finished swapping the viewavsb.refresh();It is safe to call as often as you like, and it does nothing until the snippet has finished its first evaluation.
Anti-flicker protects the first paint of a full page load. On a client-side route change the page is already visible, so the variation is applied in place. The one exception is a split URL experiment whose control URL matches the new route: the page is hidden briefly so the visitor does not watch the control render before the redirect. See Anti-Flicker.
Framework notes
React. With React Router or similar, this works out of the box. Put both tags
in your index.html (or whatever holds your root HTML).
Next.js. For App Router projects, place both tags in your root
app/layout.tsx, inside the <head>:
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> {/* A vs B stub: early-call queue + pre-paint anti-flicker hide */} <script dangerouslySetInnerHTML={{ __html: 'window.avsb=window.avsb||{};window.avsb.q=window.avsb.q||[];' + 'window.avsb.ready=window.avsb.ready||function(f){window.avsb.q.push(f)};' + "window.avsb.on=window.avsb.on||function(n,f){var t=['on',n,f];window.avsb.q.push(t);return function(){t[0]=null}};" + "window.avsb.consent=window.avsb.consent||{set:function(s){window.avsb.q.push(['consent.set',s])},get:function(){return{}}};" + "window.avsb.track=window.avsb.track||{};['event','segment','purchase','cart'].forEach(function(m){window.avsb.track[m]=window.avsb.track[m]||function(a,b){window.avsb.q.push(['track.'+m,a,b])}});" + "(function(){window.avsb._t0=Date.now();window.avsb._df=fetch('https://cdn.avsb.cloud/YOUR_SNIPPET_KEY/datafile.json').catch(function(){});var d=document,e=d.documentElement,s=d.currentScript,a=s&&s.getAttribute('data-avsb-timeout'),t=a==null?3000:+a;if(!(t>0)||window.avsb.version)return;e.style.opacity='0';e.style.pointerEvents='none';window.avsb._t=setTimeout(function(){e.style.removeProperty('opacity');e.style.removeProperty('pointer-events')},t)})();" }} /> {/* A vs B loader */} <script src="https://cdn.avsb.cloud/snippet.js?id=YOUR_SNIPPET_KEY" data-avsb="YOUR_SNIPPET_KEY" async /> </head> <body>{children}</body> </html> )}Vue. For Vue CLI or Vite apps, place both tags in your root index.html
inside the <head>.
Angular. Place both tags in src/index.html inside the <head>.
Hash-based routing
If your app routes with hash URLs (example.com/#/pricing, the default in Vue
Router's hash mode and Angular's HashLocationStrategy), switch on
Hash-based routing in Project Settings → Configuration. A vs B then treats
a changed # as a new page and also listens for the browser's hashchange event.
Leave it off for every other site. With it off, a link to #features on the
current page is correctly ignored, which is what you want: an anchor click is not
a navigation, and re-applying every experiment on each one would make the page
flicker for no reason.
The switch adds hash routes. pushState, replaceState, and the back and forward
buttons are always handled, whether it is on or off.
Checking it works
Open your browser's DevTools console on a page where an experiment runs, and read the active list:
avsb.getActiveExperiments();// [{ experimentId: '...', variationId: '...' }]Then navigate to another route in your app and run it again. On a route where the experiment does not target, the list should be empty; on a route where it does, you should get the same variation back. That round trip proves cleanup and re-evaluation are both happening.
For the full story, add ?avsb_debug=1 to the URL and navigate around with the
console open: A vs B then prints one line per experiment that did not run, naming
the rule that stopped it. See Debug Mode.