Back
Lesson 10

Communicating with Messages

View Source Code

Overview

Implement real-time communication between extension components using the Chrome messaging API. This allows the popup to notify content scripts when notes are saved, triggering instant ribbon updates without page reloads.

By the end, TabNotes will update the ribbon immediately when users save or delete notes.

What You’ll Learn

  • Understand extension messaging architecture
  • Send messages from popup directly to content scripts
  • Listen for messages in content scripts
  • Pass JSON data in messages
  • Filter messages by type and context
  • Create message-based update flows

Extension Messaging Overview

Extensions have isolated components that need to coordinate:

  • Popup - User types notes
  • Content Script - Displays ribbon on page
  • Background - Monitors tab changes (can also send messages)

Problem: These components can’t directly call each other’s functions.

Solution: Message passing - components send JSON messages directly to each other, others listen and respond.


Message Flow Architecture

User types note in popup
Popup saves to storage
Popup sends message directly to content script
Content script shows/hides ribbon

Sending Messages

Basic Message Syntax

await browser.tabs.sendMessage(tabId, messageObject);

Parameters:

  • tabId - Target tab ID
  • messageObject - Any JSON-serializable object

Common Message Structure:

{
  type: 'messageType',  // Identifies message purpose
  data: { ... }          // Message payload
}

Creating a Message Utility

// utils/sendNoteUpdateMessage.ts
import { storage } from 'wxt/storage';
import getPageKey from './getPageKey';

const sendNoteUpdateMessage = async (tabId: number) => {
  // Get tab details
  const tab = await browser.tabs.get(tabId);
  if (!tab.url) return;
  
  // Get note data for this page
  const pageKey = getPageKey(tab.url);
  const notesStorage = await storage.getItem('sync:notesStorage') || {};
  const note = notesStorage[pageKey];
  
  // Send message to content script
  const message = {
    type: 'noteUpdated',
    pageKey,
    note  // Include note data (or undefined if no note)
  };
  
  try {
    await browser.tabs.sendMessage(tabId, message);
  } catch (error) {
    // Content script might not be loaded yet (e.g., chrome:// pages)
    // Silently ignore errors
  }
};

export default sendNoteUpdateMessage;

Error Handling: Content scripts don’t run on all pages (e.g., chrome://). Catch errors gracefully.


Receiving Messages

Content scripts listen for messages using browser.runtime.onMessage:

// entrypoints/content/index.ts
import './style.css';
import getPageKey from '@/utils/getPageKey';

export default defineContentScript({
  matches: ['https://*/*'],
  main() {
    const pageKey = getPageKey(window.location.href);
    
    const showRibbon = () => {
      if (document.getElementById('tabnotes-ribbon')) return;
      
      const ribbon = document.createElement('div');
      ribbon.id = 'tabnotes-ribbon';
      ribbon.textContent = 'Has Note';
      document.body.appendChild(ribbon);
    };
    
    const hideRibbon = () => {
      const ribbon = document.getElementById('tabnotes-ribbon');
      if (ribbon) ribbon.remove();
    };
    
    // Listen for messages
    browser.runtime.onMessage.addListener((message) => {
      // Filter: only process noteUpdated messages for this page
      if (message.type !== 'noteUpdated') return;
      if (message.pageKey !== pageKey) return;
      
      // Show or hide ribbon based on note existence
      if (message.note && message.note.note) {
        showRibbon();
      } else {
        hideRibbon();
      }
    });
  }
});

Message Filtering:

  • Check message.type to ignore irrelevant messages
  • Check message.pageKey to ignore messages for other pages

Why Filter by pageKey? Messages might be sent to multiple tabs. Each content script should only respond to its own page’s updates.


Triggering Messages from Popup

When users save notes, send update messages:

// entrypoints/popup/main.ts
import changeIcon from '@/utils/changeIcon';
import sendNoteUpdateMessage from '@/utils/sendNoteUpdateMessage';

noteText.addEventListener('keyup', async () => {
  const [tab] = await browser.tabs.query({ 
    active: true, 
    currentWindow: true 
  });
  
  if (!tab.id || !tab.url) return;
  
  // Save note logic (from Lesson 6)
  const pageKey = getPageKey(tab.url);
  const notesStorage = await storage.getItem('sync:notesStorage') || {};
  
  const existingNote = notesStorage[pageKey];
  notesStorage[pageKey] = {
    note: noteText.value,
    createdAt: existingNote?.createdAt || new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };
  
  await storage.setItem('sync:notesStorage', notesStorage);
  
  // Update icon and send message
  await changeIcon(tab.id);
  await sendNoteUpdateMessage(tab.id);
});

Testing:

  1. Open popup on any page, type note
  2. Ribbon appears immediately (no reload needed)
  3. Delete note text
  4. Ribbon disappears immediately

Triggering Messages from Background

Update ribbon when users switch tabs:

// entrypoints/background.ts
import changeIcon from '@/utils/changeIcon';
import sendNoteUpdateMessage from '@/utils/sendNoteUpdateMessage';

export default defineBackground(() => {
  // Tab switched
  browser.tabs.onActivated.addListener(async (activeInfo) => {
    await changeIcon(activeInfo.tabId);
    await sendNoteUpdateMessage(activeInfo.tabId);
  });
  
  // URL changed within tab
  browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
    if (changeInfo.url && tab.active) {
      changeIcon(tabId);
      sendNoteUpdateMessage(tabId);
    }
  });
});

Complete Flow:

  1. User switches tabs
  2. Background detects via onActivated
  3. Background sends message with note data
  4. Content script shows/hides ribbon

Message Patterns

One-Way Messages (Fire and Forget)

// Sender doesn't wait for response
await browser.tabs.sendMessage(tabId, { type: 'update' });

Use Case: Notifications, status updates (like our ribbon)

Request-Response Messages

// Sender waits for response
const response = await browser.tabs.sendMessage(tabId, {
  type: 'getData'
});
console.log('Received:', response);

// Listener sends response
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'getData') {
    sendResponse({ data: 'example' });
  }
  return true;  // Required for async response
});

Use Case: Querying content script state, requesting data

TabNotes uses one-way messages - content scripts don’t need to respond.


What’s Next

Extension components now communicate in real-time. The ribbon updates instantly when notes are saved, providing immediate visual feedback without requiring page reloads.

Lesson 11 introduces custom HTML pages for extensions - creating a dedicated notes management page where users can view and edit all their saved notes in one place.