/Docs

SPA Navigation

On single-page applications, clicking a link changes the URL without reloading the page. A vs B detects these route changes and automatically re-evaluates experiments for the new URL, preserving sticky variation assignments throughout.

How route change detection works

When SPA mode is enabled for a project, the A vs B snippet patches the browser's History API. Specifically, it wraps the native pushState and replaceState methods to fire a custom event whenever the URL changes. It also listens for the popstateevent, which fires when the visitor uses the browser's Back or Forward buttons.

When any of these events fire, the snippet treats it as a navigation and begins the re-evaluation cycle.

The re-evaluation cycle

On every route change, the snippet goes through the following steps in order:

  1. Cleanup — All currently active experiment variations and Project JavaScript are torn down. This means:
    • The variation's <style> tag is removed from the <head>.
    • All timers, intervals, event listeners, and waitUntil observers started via the options self-cleaning helpers are cancelled automatically — you do not need to cancel them manually.
    • Any onRemove callbacks registered in initVariation, initTrigger, or initProject are called, allowing custom cleanup of anything the helpers cannot handle automatically (for example, DOM text reversions or third-party subscriptions).
  2. Project JavaScript re-runs — Your initProject function is called again for the new URL. This allows you to update segments, re-attach global listeners, or re-initialize anything that depends on the current page.
  3. Experiments re-evaluate — Every running experiment is evaluated against the new URL. URL targeting rules, audience conditions, and traffic allocation are all re-checked. An experiment that did not match the previous page might match the new one, and vice versa.
  4. Variation injection— Experiments that match the new URL call the visitor's assigned initVariation (and initTrigger if present). The visitor sees the new page with the correct variations applied.
Info
Anti-flicker is not re-applied during SPA navigation. Hiding and revealing the page on every route change would make your SPA feel sluggish and would interfere with transition animations. Instead, the re-evaluation happens quickly enough that no flicker is noticeable in practice. If your variation makes a dramatic above-the-fold change, use a trigger with options.waitUntilto delay activation until the target element exists in the new route's DOM.

Automatic teardown of timers, listeners, and waitUntil

Any timer, interval, event listener, or waitUntil observer started through the options helpers inside initVariation, initTrigger, or initProject is automatically cancelled when that variation or Project JS is torn down on navigation. You do not need to cancel them in onRemove.

Only resources that fall outside the helpers — native IntersectionObserver instances, third-party subscriptions, direct DOM text mutations — need an explicit onRemove callback.

Sticky bucketing across navigations

Even though experiments re-evaluate on every navigation, variation assignments are sticky. If a visitor was assigned to Variant 1 of experiment "hero-headline-test" on the homepage, and they navigate to the pricing page and then back to the homepage, they will be assigned to Variant 1 again.

This is because assignments are stored in the visitor's cookie and are deterministically derived from their visitor ID and the experiment ID. The same inputs always produce the same output.

Example: SPA-safe trigger

Use the self-cleaning helpers to avoid manual cleanup in most cases:

SPA-safe trigger using self-cleaning helpers
javascript
1function initTrigger(options, activate, deactivate) {
2 // options.waitUntil is auto-cancelled on navigation — no manual cleanup needed
3 options.waitUntil(
4 () => document.querySelector('.product-title'),
5 (titleEl) => {
6 activate();
7
8 // options.addEventListener is auto-removed on navigation
9 options.addEventListener(titleEl, 'click', () => {
10 options.track.event(99001);
11 });
12 }
13 );
14}

For resources that fall outside the helpers, register an explicit onRemove callback:

SPA-safe variation with manual DOM revert
javascript
1function initVariation(options) {
2 // Apply changes directly — initVariation runs at the activation moment
3 const titleEl = document.querySelector('.product-title');
4 if (titleEl) {
5 titleEl.dataset.originalText = titleEl.textContent;
6 titleEl.textContent = 'Try it free for 14 days';
7 }
8
9 // DOM text changes are not auto-reverted — do it manually
10 options.onRemove(() => {
11 const titleEl = document.querySelector('.product-title');
12 if (titleEl && titleEl.dataset.originalText) {
13 titleEl.textContent = titleEl.dataset.originalText;
14 }
15 });
16}

Exposure tracking on re-navigation

When a visitor is assigned to a variation, A vs B records an exposure event. On SPA navigation, if the same visitor is assigned to the same variation again (because they revisited the same page), A vs B does not record a duplicate exposure. Exposures are de-duplicated per visitor per experiment, ensuring your participant counts in the results dashboard are accurate.