Skip to main content

Overview

Hooks allow plugins to expose extension points that other plugins can register handlers for. Unlike events (fire-and-forget), hooks collect return values.
// Toolbar plugin registers a hook
api.registerHook('toolbar:add-button', (config) => {
  const btn = document.createElement('button');
  btn.textContent = config.label;
  toolbar.appendChild(btn);
  return btn;
});

// Another plugin uses the hook
const buttons = api.useHook('toolbar:add-button', {
  label: 'My Action',
  action: () => alert('clicked!')
});
// buttons → [buttonElement, ...]

API

api.registerHook(name, handler)

Register a function that responds to a named hook.
ParameterTypeDescription
namestringHook name (use namespace:action format)
handlerfunctionCallback receiving payload, can return a value
api.registerHook('sidebar:add-item', (config) => {
  const item = document.createElement('div');
  item.textContent = config.label;
  sidebarEl.appendChild(item);
  return item;
});

api.useHook(name, payload)

Call all registered handlers for a hook. Returns an array of their return values.
ParameterTypeDescription
namestringHook name to invoke
payloadanyData passed to each handler
Returns: Array<any> — return values from each handler (empty array if none registered).
const items = api.useHook('sidebar:add-item', {
  label: 'Settings',
  icon: '⚙️',
  onClick: () => openSettings()
});
// items → [divElement, divElement, ...]

api.removeHook(name, handler)

Remove a previously registered handler. Must pass the same function reference.
function myHandler(config) {
  // ...
  return result;
}

api.registerHook('my-hook', myHandler);
// Later:
api.removeHook('my-hook', myHandler);
Call removeHook in your teardown() function to clean up extension points when your plugin is disabled.

Hooks vs Events

EventsHooks
PurposeNotifyExtend
Return valuesNoYes
DirectionOne-way broadcastRequest → responses
Use case”Something happened""Add your thing to my thing”

When to Use Each

Use Events When:

  • You’re broadcasting that something happened
  • You don’t need a response
  • Multiple plugins might be interested
  • Example: todo:added, theme:changed

Use Hooks When:

  • You’re exposing an extension point
  • You need return values from handlers
  • You want plugins to contribute to your UI
  • Example: toolbar:add-button, sidebar:add-section

Timing

Hooks are typically registered during setup(), but they’re also commonly invoked after all plugins have loaded. Use the board:allPluginsLoaded event for this:
export function setup(api) {
  const toolbar = document.createElement('div');

  // Register my extension point
  api.registerHook('toolbar:add-button', (config) => {
    const btn = document.createElement('button');
    btn.textContent = config.label;
    btn.onclick = config.action;
    toolbar.appendChild(btn);
    return btn;
  });

  // After all plugins are loaded, let them add buttons
  api.bus.once('board:allPluginsLoaded', () => {
    api.useHook('toolbar:add-button', {
      label: 'Default',
      action: () => {}
    });
  });

  api.boardEl.appendChild(toolbar);
}

Example: Extensible Sidebar

// === sidebar/plugin.js ===
export function setup(api) {
  const sidebar = document.createElement('div');
  sidebar.style.cssText = `
    position: fixed; top: 60px; right: 10px;
    display: flex; flex-direction: column; gap: 8px;
  `;

  // Let plugins add sections
  api.registerHook('sidebar:add-section', (config) => {
    const section = document.createElement('div');
    section.innerHTML = `<h4>${config.title}</h4>`;
    sidebar.appendChild(section);
    return section;
  });

  // Let plugins add items
  api.registerHook('sidebar:add-item', (config) => {
    const item = document.createElement('button');
    item.textContent = `${config.icon} ${config.label}`;
    item.onclick = config.onClick;
    sidebar.appendChild(item);
    return item;
  });

  api.boardEl.appendChild(sidebar);
}

// === my-plugin/plugin.js ===
export function setup(api) {
  api.useHook('sidebar:add-section', { title: 'My Tools' });
  api.useHook('sidebar:add-item', {
    label: 'Settings',
    icon: '⚙️',
    onClick: () => openSettings()
  });
}