If your website is built with a framework like React, Angular, Vue.js, or any other SPA framework, Omniconvert Explore has you covered. SPA support is bundled directly into the Explore Tracking Code, so there’s nothing extra to install.
The challenge with SPAs is that the framework can re-render parts of the DOM at any time — potentially wiping out changes your experiment made. Explore’s SPA library solves this by using DOM Mutation Observers to monitor elements and automatically reapply your experiment’s changes whenever the framework overwrites them.
Early Access Notice: SPA support is currently in Early Access. Please contact your Omniconvert representative to have it activated for your account.
Before You Begin
Make sure the following are in place:
- SPA support is activated — contact Omniconvert customer support to enable it.
- The Explore Tracking Code is installed on your website.
- Your experiment is set up with custom code in Omniconvert Explore.
All SPA functions are available on the global window._mktz object, which is automatically initialized when the tracking code loads. You’ll be working primarily with two namespaces:
_mktz.mutationPersisters— for making persistent DOM changes_mktz.taggedTimeouts— for managing timers and event listeners safely
Core Concept: Persistent Changes
In a standard website, a jQuery $('.title').text('New title') is enough. In a SPA, the framework may re-render that element at any time and undo your change.
The solution: use _mktz.mutationPersisters functions instead. These register your changes so that Explore watches the relevant elements and automatically reapplies your modifications whenever the framework re-renders them.
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 reverted automatically.
Making Persistent DOM Changes
Modify an existing element — change()
Use this to update the content or attributes of an element that already exists on the page.
_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) |
Example:
// 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()
Use this to inject a new element into the page.
_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 |
Example:
// Add a trust badge inside a product card
_mktz.mutationPersisters.new(
".product-card",
"<span>✔ Free delivery</span>",
"div",
"trust-badge",
"lastChild"
);Insert before or after an element — before() / after()
Shorthand functions for inserting content adjacent to an element.
_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()
Replaces an element and its tag with new HTML. Useful when you need to change the element type itself, not just its content.
_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 validation on a form so you can handle it yourself in your experiment.
Apply CSS styles — css()
Sets inline CSS on an element. Existing inline styles are preserved; the new declarations are appended.
_mktz.mutationPersisters.css("#item", "background: red; font-weight: bold;");Selector Tips
All selectors support standard CSS syntax. In addition, the jQuery-style :eq(n) pseudo-class is 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
In SPAs, users navigate without full page reloads. Explore handles this by listening to URL change events (popstate, hashchange, history.pushState, history.replaceState). When a navigation is detected:
- 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 also trigger reinitialization manually — for example, if your app updates content without changing the URL:
_mktz.init();
And you can manually trigger a revert of all persister changes if needed:
_mktz.mutationPersisters.revert();
Managing Timers & Event Listeners
In a SPA, timers and event listeners can accumulate across reinitializations if not cleaned up properly. For example, if a setInterval is registered each time an experiment loads, you’ll end up with multiple intervals firing simultaneously after a few navigations.
Explore provides wrapper functions that tag your timers and listeners so they can be automatically cleaned up on reinitialization.
Timeouts
// Instead of: setTimeout(callback, 2000) _mktz.taggedTimeouts.setTaggedTimeout(callback, 2000);
Intervals
// Instead of: setInterval(callback, 1000) _mktz.taggedTimeouts.setTaggedInterval(callback, 1000);
Event Listeners
Tag your event 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
If you need finer control, you can 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');Putting It All Together — A Full Example
Here’s what a well-structured SPA experiment might look like:
// 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.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.
Need Help?
SPA support is in Early Access and actively evolving. If you run into edge cases or need functionality not covered here, reach out to your Omniconvert contact — we’re happy to help and always looking to improve based on real usage.