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:

  1. Removes the variation CSS and JavaScript from the previous page (cleaning up the old experiments).
  2. Re-runs your Project JavaScript.
  3. Re-checks every experiment against the new URL and audience rules.
  4. 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 replaceState for 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 #features on 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.

JavaScript
// After your app has finished swapping the viewavsb.refresh();
JavaScript2 lines

It is safe to call as often as you like, and it does nothing until the snippet has finished its first evaluation.

The page is not re-hidden on a route change

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>:

TypeScript React
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>  )}
TypeScript React24 lines

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.

History routing needs no setting

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:

JavaScript
avsb.getActiveExperiments();// [{ experimentId: '...', variationId: '...' }]
JavaScript2 lines

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.

Was this helpful?