How Do Userscripts Handle Dynamic Content in AJAX or React?

Userscripts often execute before a webpage finishes rendering or before dynamic content is fetched asynchronously via AJAX or framework engines like React. Because modern web applications continuously inject, update, and remove DOM elements after the initial page load, standard userscripts fail if they rely on simple single-run selectors. To reliably interact with dynamically loaded elements, userscripts must employ strategies such as dynamic polling, native browser APIs like MutationObserver, standard event listeners, or network interception techniques to detect and manipulate content as it appears.


The Challenge of Dynamic Content

Traditional web pages loaded all HTML, CSS, and dynamic content in a single synchronous HTTP request. When the DOMContentLoaded event fired, userscripts could safely assume all elements were present in the DOM.

Modern single-page applications (SPAs) built with React, Vue, or Angular work differently:


Primary Approaches to Handle Dynamic Content

1. Observing the DOM with MutationObserver

The most efficient and modern browser-native solution is the MutationObserver API. Instead of constantly checking the DOM on a timer, MutationObserver listens for specific structural changes within the document tree and fires a callback whenever nodes are added, removed, or modified.

// Function to handle target element once detected
function handleElement(node) {
    node.style.backgroundColor = 'yellow';
}

// Set up the observer
const observer = new MutationObserver((mutationsList) => {
    for (const mutation of mutationsList) {
        if (mutation.type === 'childList') {
            mutation.addedNodes.forEach((node) => {
                // Ensure it's an Element node
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node.matches('.dynamic-target-class')) {
                        handleElement(node);
                    }
                    // Search children of added nodes
                    const targets = node.querySelectorAll('.dynamic-target-class');
                    targets.forEach(handleElement);
                }
            });
        }
    }
});

// Start observing the target container or entire document
observer.observe(document.body, {
    childList: true,
    subtree: true
});

2. Polling with Utility Libraries (waitForKeyElements)

Polling involves checking the DOM at fixed intervals until a specific element exists. While raw setInterval() calls can be inefficient, established helper functions like waitForKeyElements streamline this process and handle cleanup automatically.

// Utility function for waiting on dynamically loaded elements
function waitForKeyElements(selector, callback, waitOnce = true) {
    const elements = document.querySelectorAll(selector);
    
    elements.forEach((element) => {
        if (!element.dataset.userscriptProcessed) {
            element.dataset.userscriptProcessed = 'true';
            callback(element);
        }
    });

    if (!waitOnce || elements.length === 0) {
        setTimeout(() => waitForKeyElements(selector, callback, waitOnce), 300);
    }
}

// Usage
waitForKeyElements('.react-loaded-button', (btn) => {
    btn.addEventListener('click', () => alert('Clicked!'));
});

3. Intercepting AJAX / Network Requests

Rather than waiting for React or AJAX to update the DOM, userscripts can hook into the underlying network calls by overriding window.fetch or XMLHttpRequest. This allows the script to read or modify data before the web application renders it to the screen.

// Intercepting native fetch requests
const originalFetch = window.fetch;

window.fetch = async function(...args) {
    const response = await originalFetch.apply(this, args);
    
    // Clone response to inspect data without consuming the stream
    const clone = response.clone();
    
    if (args[0].includes('/api/v1/dynamic-data')) {
        clone.json().then(data => {
            console.log('Intercepted AJAX response:', data);
            // Trigger userscript logic based on payload data
        });
    }
    
    return response;
};

4. Leveraging Event Delegation

For simple interactions (such as handling clicks on dynamically created buttons), event delegation avoids the need to watch for element creation entirely. By attaching a single event listener to a permanent parent container (like document.body), events trigger via event bubbling regardless of when the child target was added.

document.body.addEventListener('click', (event) => {
    const target = event.target.closest('.dynamic-button');
    if (target) {
        console.log('Dynamic button clicked:', target);
    }
});

Best Practices Summary

Strategy Best Used For Performance Impact
MutationObserver Detecting elements dynamically mounted by React/Vue Low (Event-driven)
Polling (setTimeout) Simple scripts or fallback detection Medium (Depends on interval)
Network Interception Reading/modifying data before rendering Very Low
Event Delegation Handling user clicks/inputs on dynamic nodes Negligible