Why does a MutationObserver not work for a Next.js app that caches data?

589 Views Asked by At

I'm developing a Chrome extension that adds a button within reoccurring, dynamically created containers on a website built with Next.js. This website uses a private API to fetch data and dynamically render components. I want to insert my button on initial page load and whenever client-side routing occurs on the website.

I can detect both the initial page load and client-side routing via JavaScript and invoke a function on load. However, I'm having trouble finding the elements I need to manipulate. When I use querySelector, the elements are not found because the components aren't rendered immediately.

To address this issue, I've tried using a MutationObserver. This works for every page on the website when I visit it for the first time, but when I load a page from the cache (as seen under Chrome Developer Tools > Application > XHR tab), the MutationObserver doesn't detect any changes, even though the complete body is emptied out and rendered again on initial page load and client-side routing.

Can anyone explain why this is happening and suggest a solution to this problem with MutationObserver?

manifest.json

{
    "manifest_version": 3,
    "name": "chatGPT UI",
    "description": "Extension to experiment with and add useful user interface elements to OpenAI's chatGPT",
    "version": "1.0",
    "icons": {
        "16": "images/icon-16.png",
        "32": "images/icon-32.png",
        "64": "images/icon-64.png",
        "128": "images/icon-128.png"
    },
    "permissions": [
        "tabs",
        "webNavigation"
    ],
    "content_scripts": [
        {
            "js": ["scripts/content.js"],
            "matches": [
                "https://chat.openai.com/*"
            ]
        }
    ]
}

content.js

// Content script is executed on initial page loads

// MutationObserver is executed for newly added elements for both the initial
// page loads and client-side routing as long as the pages have not been cached

const isChatPanelNode = (node) => {
    return node.matches('#__next > div.overflow-hidden.w-full.h-full.relative.flex.z-0 > div.relative.flex.h-full.max-w-full.flex-1 > div > main > div.flex-1.overflow-hidden > div');
};

const isEditButtonContainerNode = () => {
    return node.matches('div.text-gray-400.flex.self-end.lg\\:self-center.justify-center.mt-2.gap-2.md\\:gap-3.lg\\:gap-1.lg\\:absolute.lg\\:top-0.lg\\:translate-x-full.lg\\:right-0.lg\\:mt-0.lg\\:pl-2');
};

const getAllEditButtonContainers = () => {
    return [...document.querySelectorAll('div.text-gray-400.flex.self-end.lg\\:self-center.justify-center.mt-2.gap-2.md\\:gap-3.lg\\:gap-1.lg\\:absolute.lg\\:top-0.lg\\:translate-x-full.lg\\:right-0.lg\\:mt-0.lg\\:pl-2')];
}

const setBackgroundColor = ( element, color ) => {
    element.style.backgroundColor = color;
}

const observer = new MutationObserver((mutations) => {
    console.log(`New mutations observed`);
    mutations.forEach((record) => {
        if (record.type === 'attributes') {}
        // mutation to the tree of nodes
        if (record.type === 'childList') {
            let editButtonContainers = null;
            record.addedNodes.forEach((node) => {
                // ignore text nodes
                if (node.nodeType === 3) return;
                
                if (isChatPanelNode(node)) {
                    editButtonContainers = getAllEditButtonContainers();
                    editButtonContainers.forEach(( container ) => setBackgroundColor( container, 'yellow' ));
                }
            });
        }
    });
});

const config = {
    childList: true, // changes in the direct children of node
    subtree: true, // changes in all descendants of node
    attributes: true, // changes attributes of node
    // attributeFilter // an array of attribute names, to observe only selected ones
    characterData: false // whether to observe node.data (text content)
};

const node = document.body;
observer.observe(node, config);

Fix (content.js)

As stated by @woXXom, the callback actually fires but there was a mistake in my code as I assumed that a matching node will be present in the addedNodes NodeList. However, the MutationObserver API does not work that way. Only the common parent element of changed nodes will be included in the NodeList and the Selectors API (querySelectorAll) has to be applied on it to find the matching children.

// Content script is executed on initial page loads

// MutationObserver is executed for newly added elements for both the initial
// page loads and client-side routing as long as the pages have not been cached

const isChatPanelNode = (node) => {
    return node.matches('#__next > div.overflow-hidden.w-full.h-full.relative.flex.z-0 > div.relative.flex.h-full.max-w-full.flex-1 > div > main > div.flex-1.overflow-hidden > div');
};

const isEditButtonContainerNode = () => {
    return node.matches('div.text-gray-400.flex.self-end.lg\\:self-center.justify-center.mt-2.gap-2.md\\:gap-3.lg\\:gap-1.lg\\:absolute.lg\\:top-0.lg\\:translate-x-full.lg\\:right-0.lg\\:mt-0.lg\\:pl-2');
};

const getAllEditButtonContainers = (element) => {
    return [...element.querySelectorAll('div.group.w-full.text-gray-800.dark\\:text-gray-100.border-b.border-black\\/10.dark\\:border-gray-900\\/50.dark\\:bg-gray-800 > div div > div.text-gray-400.flex.self-end.lg\\:self-center.justify-center.mt-2.gap-2.md\\:gap-3.lg\\:gap-1.lg\\:absolute.lg\\:top-0.lg\\:translate-x-full.lg\\:right-0.lg\\:mt-0.lg\\:pl-2')];
}

const setBackgroundColor = ( element, color ) => {
    element.style.backgroundColor = color;
}

const observer = new MutationObserver((mutations) => {
    console.log(`New mutations observed`);
    mutations.forEach((record) => {
        let editButtonContainers = null;
        if (record.type === 'attributes') {
        }
        // mutation to the tree of nodes
        if (record.type === 'childList') {
            record.addedNodes.forEach((node) => {
                if (node.nodeType === 3) return; // ignore text nodes

                editButtonContainers = getAllEditButtonContainers(node);
                editButtonContainers.forEach(editButtonContainer => setBackgroundColor(editButtonContainer, 'yellow'));
            });
        }
    });
});

const config = {
    childList: true, // changes in the direct children of node
    subtree: true, // changes in all descendants of node
    attributes: true, // changes attributes of node
    // attributeFilter // an array of attribute names, to observe only selected ones
    characterData: false // whether to observe node.data (text content)
};

const node = document;
observer.observe(node, config);
0

There are 0 best solutions below