Set Up Single-Page Application (SPA) Support

Overview

If your website is built with a framework like React, Angular, Vue.js, or any other SPA framework, Explore has you covered. SPA support is bundled directly into the Explore tracking code - nothing extra to install.

Note

SPA support is enabled by default for new accounts, and enabled for most existing accounts. If you're on an older account and it isn't active yet, contact your Omniconvert representative to have it turned on.

The problem SPA support solves

SPA frameworks can re-render parts of the DOM at any time, potentially wiping out changes your experiment made. Explore's SPA library solves this using DOM Mutation Observers to monitor elements and automatically reapply your experiment's changes whenever the framework overwrites them.

Before you begin

Make sure the following are in place:

  • SPA support is active on your account.

  • The Explore tracking code is installed on your website.

  • Your experiment is set up with custom code.

All SPA functions are available on the global window._mktz object, automatically initialized when the tracking code loads. You'll mainly work with two namespaces:

  • _mktz.mutationPersisters - for making persistent DOM changes.

  • _mktz.taggedTimeouts - for managing timers and event listeners safely.

Core concept: Persistent changes

On a standard website, $('.title').text('New title') is enough. In a SPA, the framework may re-render that element at any time and undo your change.

Warning

Only changes made through _mktz.mutationPersisters are tracked by Explore's rollback and reapplication system. Direct jQuery or DOM changes will not be persisted or automatically reverted.

Making persistent DOM changes

Modify an existing element - change()

_mktz.mutationPersisters.change(selector, newContent, attributes);

Argument

Description

selector

CSS selector for the target element

newContent

New inner HTML as a string, or an array of HTML strings

attributes

Object of HTML attributes to set (optional)

// Change a product title and update a data attribute
_mktz.mutationPersisters.change(
".product_title",
"<b>New title</b>",
{ "data-id": "99" }
);

// Content only, no attribute changes
_mktz.mutationPersisters.change(".hero-headline", "Summer Sale - Up to 50% Off");

Add a new element - new()

_mktz.mutationPersisters.new(container, content, nodeName, className, placement);

Argument

Description

container

CSS selector for the parent element

content

Inner HTML of the new element

nodeName

HTML tag to use (e.g. "div", "p", "span")

className

CSS class(es) to apply (optional)

placement

Where to insert: lastChild (default), firstChild, before, after

// Change a product title and update a data attribute
_mktz.mutationPersisters.change(
".product_title",
"<b>New title</b>",
{ "data-id": "99" }
);

// Content only, no attribute changes
_mktz.mutationPersisters.change(".hero-headline", "Summer Sale - Up to 50% Off");

Insert before or after an element - before() / after()

_mktz.mutationPersisters.before("#product-price", "<p class='badge'>Best Seller</p>");
_mktz.mutationPersisters.after("#product-price", "<p class='note'>Incl. VAT</p>");

Remove an element - remove()

Removes an element and keeps it removed even if the framework re-renders.

_mktz.mutationPersisters.remove("#promotional-banner");

Replace an element entirely - replaceWith()

_mktz.mutationPersisters.replaceWith(
".product_title",
"<h2 class='v2'>New Product Title</h2>"
);
Note

Any <script> tags in the replacement HTML are executed, and re-run if the change is reapplied.

Set or remove HTML attributes - attr() / removeAttr()

// Add or update an attribute
_mktz.mutationPersisters.attr('.product_item', 'data-role', 'product');

// Remove an attribute (and keep it removed)
_mktz.mutationPersisters.removeAttr('form.signup', 'novalidate');
Note

A common use case for removeAttr is disabling native browser form validation so you can handle it yourself in your experiment.

Apply CSS styles - css()

Sets inline CSS on an element. Existing inline styles are preserved; new declarations are appended.

_mktz.mutationPersisters.css("#item", "background: red; font-weight: bold;");

Selector tips

All standard CSS selectors work. The jQuery-style :eq(n) pseudo-class is also supported, to target a specific element by index (zero-based):

// Target the first .product element
_mktz.mutationPersisters.change(".product:eq(0)", "Featured Product");

// Target the third item in a list
_mktz.mutationPersisters.remove(".nav-item:eq(2)");

How reinitialization works

SPA navigation happens without full page reloads. Explore handles this by listening for URL change events (popstate, hashchange, history.pushState, history.replaceState).

On navigation:

  1. All previous experiment changes are automatically reverted.

  2. Explore reinitializes and evaluates active experiments against the new URL and segmentation rules.

  3. Qualifying experiments are reapplied.

You can trigger reinitialization manually - useful if your app updates content without changing the URL:

_mktz.init();

And manually revert all persister changes if needed:

_mktz.mutationPersisters.revert();

Managing timers & event listeners

In a SPA, timers and listeners can accumulate across reinitializations if not cleaned up - e.g. a setInterval registered each time an experiment loads results in multiple intervals firing simultaneously after a few navigations.

Timeouts and intervals

// Instead of: setTimeout(callback, 2000)
_mktz.taggedTimeouts.setTaggedTimeout(callback, 2000);

// Instead of: setInterval(callback, 1000)
_mktz.taggedTimeouts.setTaggedInterval(callback, 1000);

Event listeners

Tag listeners with the exploreScrollEvent namespace - Explore automatically removes all listeners using this tag on reinitialization.

// Instead of: mktz_$('#my-button').on('click', handleClick)
mktz_$('#my-button').on('click.exploreScrollEvent', handleClick);

Custom tags

For finer control, use a custom tag and clear it manually:

_mktz.taggedTimeouts.setTaggedTimeout(callback, 2000, 'my-experiment-tag');
_mktz.taggedTimeouts.setTaggedInterval(callback, 1000, 'my-experiment-tag');

// Clear all timers with this tag (both timeouts and intervals)
_mktz.taggedTimeouts.clearTaggedTimers('my-experiment-tag');

Full example

// 1. Make persistent DOM changes
_mktz.mutationPersisters.change(
".product_title",
"New Improved Title",
{ "data-variant": "b" }
);

_mktz.mutationPersisters.new(
".product-actions",
"<p class='urgency-msg'>Only 3 left in stock!</p>",
"div",
"urgency-wrapper",
"firstChild"
);

_mktz.mutationPersisters.css(".add-to-cart-btn", "background: #e63946; color: #fff;");

// 2. Use tagged timers to avoid duplication on reinit
_mktz.taggedTimeouts.setTaggedTimeout(function () {
_mktz.mutationPersisters.change(".promo-banner", "Flash Sale ends in 10 minutes!");
}, 3000);

// 3. Use namespaced listeners for safe cleanup
mktz_$(document).on('click.exploreScrollEvent', '.add-to-cart-btn', function () {
var productId = mktz_$(this).closest('[data-product-id]').data('product-id');
console.log('Added to cart:', productId);
});

Quick reference

Task

Function

Modify element content/attributes

_mktz.mutationPersisters.change()

Insert a new element

_mktz.mutationPersisters.new()

Insert before an element

_mktz.mutationPersisters.before()

Insert after an element

_mktz.mutationPersisters.after()

Remove an element

_mktz.mutationPersisters.remove()

Replace an element entirely

_mktz.mutationPersisters.replaceWith()

Set an attribute

_mktz.mutationPersisters.attr()

Remove an attribute

_mktz.mutationPersisters.removeAttr()

Apply inline CSS

_mktz.mutationPersisters.css()

Revert all persister changes

_mktz.mutationPersisters.revert()

Trigger manual reinitialization

_mktz.init()

Safe timeout

_mktz.taggedTimeouts.setTaggedTimeout()

Safe interval

_mktz.taggedTimeouts.setTaggedInterval()

Clear custom-tagged timers

_mktz.taggedTimeouts.clearTaggedTimers()

Safe event listener

mktz_$(...).on('event.exploreScrollEvent', handler)

Key rules to remember

  • Always use _mktz.mutationPersisters for DOM changes - direct jQuery changes won't be persisted or reverted automatically.

  • Always use tagged timers and namespaced listeners - untagged ones will stack up across reinitializations.

  • Call _mktz.init() if your app updates content without a URL change - this tells Explore to treat it as a new page view.

  • The :eq(n) pseudo-selector is your friend - use it to target a specific element when multiple match the same selector.

Updated