SPA Issues
In a single-page application (SPA), the browser does not perform a full page load when users navigate between routes. Instead, JavaScript updates the URL and re-renders content. This can cause A vs B experiments to stop working after the first navigation. The variation (the version a visitor sees, control or a challenger) can disappear. Or the experiment does not re-trigger. Or events stop firing.
Symptoms
- The experiment variation is visible on the initial page load, but disappears after clicking a link or navigating to another route.
- Experiment data accumulates only on the first page visit, not on return visits to the same route via SPA navigation.
- URL targeting that works on a full reload does not trigger on in-app navigation.
- Pageview metrics do not fire on SPA route changes.
There is nothing to switch on
Client-side navigation is handled for every project, out of the box. If experiments stop working after an in-app navigation, the cause is somewhere else on this page, not a setting you forgot.
How it works
The snippet patches two browser APIs to intercept navigation events:
history.pushState(): called when a SPA navigates forward to a new route.history.replaceState(): called when a SPA replaces the current route without adding to the browser history.
The snippet also listens for the popstate event, which fires when the visitor presses the browser's back or forward button.
After each navigation event, the snippet re-evaluates all running experiments against the new URL. Targeting rules are the conditions that decide which visitors see which variation, and the snippet checks them again on every route change. An experiment whose rules match the new URL activates. One that no longer matches deactivates, and its CSS is removed. This means:
- An experiment targeting
/pricingwill activate when the visitor navigates to/pricingand deactivate when they navigate away from it. - An experiment targeting
/with a Substring match will remain active across all routes, because every URL contains/.
The snippet waits 50 milliseconds after a navigation event before it reacts. So several pushState or replaceState calls in the same tick only trigger one re-evaluation, against the URL as it settles.
Two cases are deliberately ignored, because neither is a new page. One is the same URL being set again: apps often call replaceState for scroll bookkeeping, not for a real navigation. The other is a changed #fragment, unless you have switched on hash-based routing.
Add ?avsb_debug=1 to the URL and navigate around with the DevTools console open. A vs B prints one line per experiment that did not run, naming the rule that stopped it and the values involved. See Debug Mode.
Framework-specific notes
React Router
React Router uses history.pushState() for all navigation. Nothing to configure.
Next.js
Next.js App Router uses history.pushState() for client-side navigation. Nothing to configure. Note that Next.js also supports server-side rendering, so the initial page load is a full server render; the snippet behaves correctly for both.
Vue Router
Vue Router in history mode uses pushState() and is handled with no configuration. Vue Router in hash mode (URLs like example.com/#/about) changes only the fragment, which is not treated as navigation until you switch on hash-based routing. See below.
Hash-based routing
If your application routes with hash URLs (the URL contains # before the path), switch on Hash-based routing in Project Settings → Configuration. A vs B then treats a changed # as a new page and listens for the browser's hashchange event as well.
Leave it off for every other site. With it off, a link to #features on the current page is correctly ignored. An anchor click does not tear down and re-apply your experiments.
Navigation the browser never sees
Some apps change what the page shows without touching the URL at all: a multi-step form, a modal route, a locale switch. Call avsb.refresh() at that moment and A vs B re-evaluates everything against the page as it is now.
// After your app has finished swapping the viewavsb.refresh();Experiments deactivating on navigation
When a visitor navigates to a page that no longer matches the experiment's targeting rules, the experiment deactivates. This is expected behaviour: the experiment should only run on the pages it is targeting. The variation's CSS is removed from the page and the original appearance is restored.
If you do not want the experiment to deactivate on navigation away from the target page, you have a few options:
- Broaden the URL targeting to include the pages you want the variation to persist on.
- Use JavaScript in your variation code to make changes that are not purely CSS. For example, modify the DOM directly, or store a flag in sessionStorage that persists across navigations.
An experiment targeting a specific page should not affect other pages. If your experiment is targeting /checkout and the visitor navigates to /home, the checkout variation correctly stops applying. If you want the variation to persist, widen the URL targeting rule, for example change it to a Substring match on / to cover all pages.
Navigate to the target route via in-app navigation, not a full reload. Then open the DevTools console and run avsb.getVariation(42) with your Experiment ID. If it returns null, the experiment is not matching the current URL. Compare the URL in the address bar to the targeting rule in Step 1 of the experiment builder. Or add ?avsb_debug=1 and let A vs B name the failing rule for you.