Split Test Pro
Advanced 5 min read

Single-Page Apps

Split Test Pro evaluates targeting on initial page load only — SPA route changes via pushState don't retrigger experiments. Learn the limitation and the workaround.

If your site is a single-page application (SPA) — React, Vue, Svelte, Angular, Nuxt, Next.js, Astro client islands — there’s an important limitation to know about. This guide covers what’s affected, why, and what to do about it.

The Limitation

Split Test Pro’s HTML script binds variants to the page on initial load:

  1. The page loads.
  2. The script runs, fetches active experiments, evaluates URL targeting against window.location, and applies any matching variants.
  3. If the visitor navigates within the SPA via client-side routing (history.pushState, React Router, Vue Router, etc.), the script does not re-evaluate.

Concretely:

  • A variant on /products/widget activates correctly when the visitor lands there directly.
  • The same visitor clicking a link to /products/widget-v2 (without a full page reload) doesn’t trigger a fresh targeting evaluation.
  • If /products/widget-v2 has a different active experiment, that experiment’s variant won’t apply.
  • The variant from /products/widget may still be applied to the new route (the CSS / JS injection persists in the DOM).

The script does not listen for popstate, pushState, or any custom router events.

This affects the HTML platform. The Shopify integration is not affected the same way — Shopify storefronts are mostly server-rendered, and full-page navigations are the norm.

Why It Works This Way

A few honest reasons:

  • Performance — auto-listening for every history change adds overhead even on traditional sites that never use SPA navigation.
  • Compatibility — different SPA frameworks dispatch different events; there’s no single hook that works everywhere.
  • Predictability — re-running variant assignment mid-session could change which variant a visitor sees, which is a worse experience than not running at all.

Re-evaluation on route change is a meaningful feature to add and is on the roadmap. Until then, the workarounds below cover the common cases.

The Right Workaround Depends on Your Setup

Different SPA architectures need different approaches.

Approach 1: Force a full page reload on key navigations

The simplest fix. If your SPA can fall back to full-page navigation for certain links (e.g., links between major sections), the script re-evaluates on the load.

In React Router:

{/* For most internal links, use Link as normal */}
<Link to="/about">About</Link>

{/* For links to pages with experiments, use a regular anchor: */}
<a href="/products/widget">Widget</a>

In Next.js:

{/* Most pages: */}
<Link href="/about">About</Link>

{/* Pages with experiments: bypass the client router */}
<a href="/products/widget">Widget</a>

Trade-off: full-page navigation is slower than client routing. Use this approach only if the affected pages are infrequent destinations or the slowdown is acceptable.

Approach 2: Reload programmatically on SPA route change

If you can’t avoid client-side routing, force a reload when entering a route with an experiment.

React Router v6:

import { useEffect } from "react";
import { useLocation } from "react-router-dom";

function ExperimentRouteWrapper({ children }) {
  const location = useLocation();
  useEffect(() => {
    // Force full reload if this route is configured for experiments
    if (location.pathname.startsWith("/products/")) {
      window.location.reload();
    }
  }, [location.pathname]);
  return children;
}

Wrap your experiment-eligible routes in this component. The first SPA navigation in triggers a reload, after which you’re effectively on a regular page-load cycle.

Vue Router:

router.afterEach((to, from) => {
  if (to.path.startsWith("/products/")) {
    window.location.reload();
  }
});

Approach 3: Re-fire the script’s init manually (advanced)

If neither full-reload approach is acceptable and you control your build, you can re-trigger the script’s targeting pass manually. The HTML script doesn’t expose a public re-init API today, but a workaround:

// On SPA route change, dispatch a custom event the script could listen for.
// This requires the script to be patched with a listener — not yet built in.
// File a feature request if this is your need.

This isn’t a supported pattern today; mentioned only because some teams have asked. The cleanest path is Approach 1 or 2.

Approach 4: Treat each SPA “page” as a distinct experiment surface

Rather than fighting the SPA’s behavior, design your experiments around it:

  • Run experiments on the landing page of a flow only — the page where visitors first arrive via direct URL or search. SPA navigations after that are “in-flow” and don’t get experiment-affected.
  • Use event activation to trigger variants on specific user interactions rather than on page load. The activation fires regardless of routing.
  • For tests that need to span multiple SPA routes, use a CSS variant in the global app shell that targets elements via class or data attribute — the variant applies once and persists across route changes.

This isn’t a workaround so much as a reframing: SPAs and per-page A/B testing have a fundamental impedance mismatch, and designing around it produces cleaner results than retrofitting.

What About Events?

The activation/targeting limitation doesn’t affect conversion tracking. SplitTestPro.trackConversion(eventKey) works fine in an SPA context — it reads the visitor’s current cookie assignment and posts the event. As long as the visitor was assigned at some point during the session (on initial load or after a forced reload), tracking events later in the SPA flow attributes correctly.

The limitation is purely about which experiments apply to which routes. The data model handles cross-route conversion correctly.

Common Setup Issues in SPAs

A few SPA-specific gotchas:

Variant CSS gets re-rendered out

Some frameworks (especially Vue with scoped styles) re-render the DOM aggressively on route changes, which can wipe inline styles applied by the variant. If your variant works briefly and then disappears, see Troubleshooting “Variant CSS shows briefly, then disappears.”

Variant JS runs against an empty DOM

If your variant JS targets an element that’s rendered by the SPA framework asynchronously, the JS may run before the element exists. Use a MutationObserver pattern — see JavaScript Variants.

The script loads but never sees a route match

If your SPA uses hash-based routing (/#products/widget) instead of pushState routing (/products/widget), the script’s URL targeting evaluates against the hash. URL-fragment targeting is currently not fully wired (see URL Targeting) — for now, target on Full URL contains instead of fragment-specific rules.

Frameworks Tested

The HTML script has been confirmed to work on initial load with:

  • Next.js (both app/ and pages/ routers, both server and client components)
  • React Router v6
  • Vue Router v4
  • Astro with React/Vue/Svelte islands
  • Nuxt 3
  • SvelteKit
  • Any framework that produces a page with a <head> and a real URL

The key is that on initial pageload, the script runs synchronously and applies variants. The SPA’s subsequent behavior is up to the framework.

When This Limitation Doesn’t Matter

If your SPA mostly:

  • Receives traffic from search / email / direct URL (so visitors enter via initial pageload).
  • Has experiments only on landing pages, not deep-linked internal flows.
  • Treats route changes as in-flow continuations rather than distinct experiment surfaces.

…then the limitation effectively doesn’t apply. Most SaaS sites fit this pattern — landing pages are where experiments matter, and post-signup flows are not typically experiment surfaces.

If your SPA has experiments throughout an extended in-app journey (e.g., a multi-step onboarding), the workarounds above become essential.

Roadmap

A built-in SPA mode that re-evaluates targeting on pushState is on the roadmap. When it ships, this doc will be updated to describe the supported pattern. Until then, the workarounds above are your tools.

Next Steps

Ready to start testing?

Install Split Test Pro and run your first experiment today.

Install on Shopify