Skip to content

bug: app.onhostcontextchanged handlers silently overwrite each other #225

@antonpk1

Description

@antonpk1

Summary

Setting app.onhostcontextchanged multiple times silently overwrites previous handlers. This breaks real-time theme updates and any custom host context handling.

Root Cause

The setter calls setNotificationHandler() which replaces the previous handler:

// app.ts line 510-521
set onhostcontextchanged(callback) {
  this.setNotificationHandler(
    McpUiHostContextChangedNotificationSchema,
    (n) => {
      this._hostContext = { ...this._hostContext, ...n.params };
      callback(n.params);  // Only ONE callback ever runs
    },
  );
}

Breaking Examples

Example 1: useHostStyles internal conflict

// User writes this simple code:
function MyWidget() {
  const { app } = useApp({ ... });
  useHostStyles(app, hostContext);  // Expects theme updates to work
  return <div>...</div>;
}

// BREAKS because inside useHostStyles:
useHostStyleVariables(app, ...);  // Sets handler A (theme + variables)
useHostFonts(app, ...);           // Sets handler B (fonts only) - OVERWRITES A!

// Result: Theme changes → only fonts handler runs → theme never updates

Example 2: User handler + SDK hook

function MyWidget() {
  const { app } = useApp({ ... });
  const [ctx, setCtx] = useState(null);

  useEffect(() => {
    if (!app) return;
    // User sets their own handler
    app.onhostcontextchanged = (params) => {
      setCtx(prev => ({ ...prev, ...params }));  // Update React state
      trackAnalytics("theme_changed", params);   // Custom logic
    };
  }, [app]);

  // Later, user adds SDK hook for convenience
  useHostStyles(app, ctx);  // OVERWRITES user handler!

  // Result: setCtx() never called, trackAnalytics() never called
}

Example 3: Two SDK hooks

function MyWidget() {
  const { app } = useApp({ ... });

  // User wants variables but not fonts
  useHostStyleVariables(app, ctx);

  // User also wants to track theme changes
  useEffect(() => {
    app.onhostcontextchanged = (params) => {
      console.log("Theme changed to:", params.theme);  // OVERWRITES SDK hook!
    };
  }, [app]);

  // Result: Variables never applied, only console.log runs
}

Example 4: Multiple custom handlers (impossible today)

function MyWidget() {
  const { app } = useApp({ ... });

  // Component A wants to know about theme
  useEffect(() => {
    app.onhostcontextchanged = (params) => {
      updateComponentA(params);
    };
  }, [app]);

  // Component B also wants to know about theme  
  useEffect(() => {
    app.onhostcontextchanged = (params) => {
      updateComponentB(params);  // OVERWRITES Component A!
    };
  }, [app]);

  // Result: Only Component B gets updates
}

Symptoms

  • Dark/light mode toggle does not update widget in real-time
  • Custom onhostcontextchanged handlers stop working after adding SDK hooks
  • No errors in console (silent failure)
  • Requires page refresh to see changes

Proposed Fix

Change the setter to chain handlers instead of replacing:

private _hostContextChangedHandlers: Array<(params) => void> = [];

set onhostcontextchanged(callback) {
  this._hostContextChangedHandlers.push(callback);
  
  // Only register the notification handler once
  if (this._hostContextChangedHandlers.length === 1) {
    this.setNotificationHandler(
      McpUiHostContextChangedNotificationSchema,
      (n) => {
        this._hostContext = { ...this._hostContext, ...n.params };
        for (const handler of this._hostContextChangedHandlers) {
          handler(n.params);
        }
      },
    );
  }
}

Workaround

Until fixed, use a single combined handler instead of SDK hooks:

useEffect(() => {
  if (!app) return;
  
  const ctx = app.getHostContext();
  if (ctx?.theme) applyDocumentTheme(ctx.theme);
  if (ctx?.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
  if (ctx?.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);

  app.onhostcontextchanged = (params) => {
    if (params.theme) applyDocumentTheme(params.theme);
    if (params.styles?.variables) applyHostStyleVariables(params.styles.variables);
    if (params.styles?.css?.fonts) applyHostFonts(params.styles.css.fonts);
    // Add any custom logic here too
  };
}, [app]);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions