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.
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.
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 |
|
|
CSS selector for the target element |
|
|
New inner HTML as a string, or an array of HTML strings |
|
|
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 |
|
|
CSS selector for the parent element |
|
|
Inner HTML of the new element |
|
|
HTML tag to use (e.g. |
|
|
CSS class(es) to apply (optional) |
|
|
Where to insert: |
// 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>"
);
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');
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:
-
All previous experiment changes are automatically reverted.
-
Explore reinitializes and evaluates active experiments against the new URL and segmentation rules.
-
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 |
|
|
Insert a new element |
|
|
Insert before an element |
|
|
Insert after an element |
|
|
Remove an element |
|
|
Replace an element entirely |
|
|
Set an attribute |
|
|
Remove an attribute |
|
|
Apply inline CSS |
|
|
Revert all persister changes |
|
|
Trigger manual reinitialization |
|
|
Safe timeout |
|
|
Safe interval |
|
|
Clear custom-tagged timers |
|
|
Safe event listener |
|
Key rules to remember
-
Always use
_mktz.mutationPersistersfor 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.