Article sections

    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:

    1. SPA support is activated — contact Omniconvert customer support to enable it.
    2. The Explore Tracking Code is installed on your website.
    3. 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);
    ArgumentDescription
    selectorCSS selector for the target element
    newContentNew inner HTML as a string, or an array of HTML strings
    attributesObject 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);
    ArgumentDescription
    containerCSS selector for the parent element
    contentInner HTML of the new element
    nodeNameHTML tag to use (e.g. "div", "p", "span")
    classNameCSS class(es) to apply (optional)
    placementWhere 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:

    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 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

    TaskFunction
    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 listenermktz_$(...).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.

    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.

    Was this post helpful?