diff --git a/apps/client/src/components/component.ts b/apps/client/src/components/component.ts index 56c9e7b79..1538aff76 100644 --- a/apps/client/src/components/component.ts +++ b/apps/client/src/components/component.ts @@ -57,6 +57,18 @@ export class TypedComponent> { return this; } + /** + * Removes a child component from this component's children array. + * This is used for cleanup when a widget is unmounted to prevent event listener accumulation. + */ + removeChild(component: ChildT) { + const index = this.children.indexOf(component); + if (index !== -1) { + this.children.splice(index, 1); + component.parent = undefined; + } + } + handleEvent(name: T, data: EventData): Promise | null | undefined { try { const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data); diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 215742046..6c5b31061 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -634,7 +634,8 @@ export function useLegacyWidget(widgetFactory: () => T, { const ref = useRef(null); const parentComponent = useContext(ParentComponent); - // Render the widget once. + // Render the widget once - note that noteContext is intentionally NOT a dependency + // to prevent creating new widget instances on every note switch. const [ widget, renderedWidget ] = useMemo(() => { const widget = widgetFactory(); @@ -642,14 +643,21 @@ export function useLegacyWidget(widgetFactory: () => T, { parentComponent.child(widget); } - if (noteContext && widget instanceof NoteContextAwareWidget) { - widget.setNoteContextEvent({ noteContext }); - } - const renderedWidget = widget.render(); return [ widget, renderedWidget ]; - }, [ noteContext, parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps - // widgetFactory() is intentionally left out + }, [ parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps + // widgetFactory() and noteContext are intentionally left out - widget should be created once + // and updated via activeContextChangedEvent when noteContext changes. + + // Cleanup: remove widget from parent's children when unmounted + useEffect(() => { + return () => { + if (parentComponent) { + parentComponent.removeChild(widget); + } + widget.cleanup(); + }; + }, [ parentComponent, widget ]); // Attach the widget to the parent. useEffect(() => { @@ -660,10 +668,17 @@ export function useLegacyWidget(widgetFactory: () => T, { } }, [ renderedWidget ]); - // Inject the note context. + // Inject the note context - this updates the existing widget without recreating it. + // We check if the context actually changed to avoid double refresh when the event system + // also delivers activeContextChanged to the widget through component tree propagation. useEffect(() => { if (noteContext && widget instanceof NoteContextAwareWidget) { - widget.activeContextChangedEvent({ noteContext }); + // Only trigger refresh if the context actually changed. + // The event system may have already updated the widget, in which case + // widget.noteContext will already equal noteContext. + if (widget.noteContext !== noteContext) { + widget.activeContextChangedEvent({ noteContext }); + } } }, [ noteContext, widget ]);