Pass a variable from content script to popup

As you have properly noticed, you can’t send data directly to the popup when it’s closed. So, you’re sending data to the background page.

Then, when you open the popup, you want the data there. So, what are the options?

Please note: this answer will give bad advice first, and then improve on it. Since OP is learning, it’s important to show the thought process and the roadbumps.


First solution that comes to mind is the following: ask the background page, using Messaging again. Early warning: this will not work or work poorly

First off, establish that there can be different types of messages. Modifying your current messaging code:

// content.js
chrome.runtime.sendMessage({type: "setCount", count: count});

// background.js
chrome.runtime.onMessage.addListener(
    function(message, sender, sendResponse) {
        switch(message.type) {
            case "setCount":
                temp = message.count;
                break;
            default:
                console.error("Unrecognised message: ", message);
        }
    }
);

And now, you could in theory ask that in the popup:

// popup.js
chrome.runtime.sendMessage({type: "getCount"}, function(count) {
    if(typeof count == "undefined") {
        // That's kind of bad
    } else {
        // Use count
    }
});

// background.js
chrome.runtime.onMessage.addListener(
    function(message, sender, sendResponse) {
        switch(message.type) {
            case "setCount":
                temp = message.count;
                break;
            case "getCount":
                sendResponse(temp);
                break;
            default:
                console.error("Unrecognised message: ", message);
        }
    }
);

Now, what are the problems with this?

  1. What’s the lifetime of temp? You have explicitly stated "persistent": false in your manifest. As a result, the background page can be unloaded at any time, wiping state such as temp.

    You could fix it with "persistent": true, but keep reading.

  2. Which tab’s count do you expect to see? temp will have the last data written to it, which may very well not be the current tab.

    You could fix it with keeping tabs (see what I did there?) on which tab sent the data, e.g. by using:

    // background.js
    /* ... */
      case "setCount":
          temp[sender.tab.id] = message.count;
          break;
      case "getCount":
          sendResponse(temp[message.id]);
          break;
    
    // popup.js
    chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
        // tabs is a single-element array after this filtering
        chrome.runtime.sendMessage({type: "getCount", id: tabs[0].id}, function(count) {
            /* ... */
        });
    });
    

    It’s a lot of work though, isn’t it? This solution works fine though for non-tab-specific data, after fixing 1.


Next improvement to consider: do we need the background page to store the result for us? After all, chrome.storage is a thing; it’s a persistent storage that all extension scripts (including content scripts) can access.

This cuts the background (and Messaging) out of the picture:

// content.js
chrome.storage.local.set({count: count});

// popup.js
chrome.storage.local.get("count", function(data) {
    if(typeof data.count == "undefined") {
        // That's kind of bad
    } else {
        // Use data.count
    }
});

This looks cleaner, and completely bypasses problem 1 from above, but problem 2 gets trickier. You can’t directly set/read something like count[id] in the storage, you’ll need to read count out, modify it and write it back. It can get slow and messy.

Add to that that content scripts are not really aware of their tab ID; you’ll need to message background just to learn it. Ugh. Not pretty. Again, this is a great solution for non-tab-specific data.


Then the next question to ask: why do we even need a central location to store the (tab-specific) result? The content script’s lifetime is the page’s lifetime. You can ask the content script directly at any point. Including from the popup.

Wait, wait, didn’t you say at the very top you can’t send data to the popup? Well, yes, kinda: when you don’t know if it’s there listening. But if the popup asks, then it must be ready to get a response, no?

So, let’s reverse the content script logic. Instead of immediately sending the data, wait and listen for requests:

chrome.runtime.onMessage.addListener(
    function(message, sender, sendResponse) {
        switch(message.type) {
            case "getCount":
                sendResponse(count);
                break;
            default:
                console.error("Unrecognised message: ", message);
        }
    }
);

Then, in the popup, we need to query the tab that contains the content script. It’s a different messaging function, and we have to specify the tab ID.

    chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
        chrome.tabs.sendMessage(tabs[0].id, {type: "getCount"}, function(count) {
            /* ... */
        });
    });

Now that’s much cleaner. Problem 2 is solved: we query the tab we want to hear from. Problem 1 seems to be solved: as long as a script counted what we need, it can answer.

Do note, as a final complication, that content scripts are not always injected when you expect them to: they only start to activate on navigation after the extension was (re)loaded. Here’s an answer explaining that in great detail. It can be worked around if you wish, but for now just a code path for it:

function(count) {
    if(typeof count == "undefined") {
        // That's kind of bad
        if(chrome.runtime.lastError) {
            // We couldn't talk to the content script, probably it's not there
        }
    } else {
        // Use count
    }
}

Leave a Comment