From 267a37d3bde82343bc514d3da508b22bf26cb094 Mon Sep 17 00:00:00 2001 From: lzinga Date: Tue, 30 Dec 2025 13:59:46 -0800 Subject: [PATCH 1/3] feat(component): add removeChild method for cleanup of child components feat(hooks): improve useLegacyWidget cleanup and memoization logic --- apps/client/src/components/component.ts | 12 ++++++++++++ apps/client/src/widgets/react/hooks.tsx | 25 +++++++++++++++++-------- 2 files changed, 29 insertions(+), 8 deletions(-) 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..d04fbe60b 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,9 +668,10 @@ export function useLegacyWidget(widgetFactory: () => T, { } }, [ renderedWidget ]); - // Inject the note context. + // Inject the note context - this updates the existing widget without recreating it. useEffect(() => { if (noteContext && widget instanceof NoteContextAwareWidget) { + widget.setNoteContextEvent({ noteContext }); widget.activeContextChangedEvent({ noteContext }); } }, [ noteContext, widget ]); From b936a35b63a73ce888c2e7d14deac742580c443b Mon Sep 17 00:00:00 2001 From: lzinga Date: Wed, 31 Dec 2025 07:31:22 -0800 Subject: [PATCH 2/3] fix(widget): prevent unnecessary refresh by checking note context change --- apps/client/src/widgets/react/hooks.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index d04fbe60b..f0019ac46 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -669,10 +669,17 @@ export function useLegacyWidget(widgetFactory: () => T, { }, [ renderedWidget ]); // 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.setNoteContextEvent({ noteContext }); - 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.setNoteContextEvent({ noteContext }); + widget.activeContextChangedEvent({ noteContext }); + } } }, [ noteContext, widget ]); From 9879d07becd644c0ac89f234b5b0689fe9c933cc Mon Sep 17 00:00:00 2001 From: lzinga Date: Thu, 1 Jan 2026 11:05:30 -0800 Subject: [PATCH 3/3] fix(widget): remove redundant note context update in useLegacyWidget --- apps/client/src/widgets/react/hooks.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index f0019ac46..6c5b31061 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -677,7 +677,6 @@ export function useLegacyWidget(widgetFactory: () => T, { // The event system may have already updated the widget, in which case // widget.noteContext will already equal noteContext. if (widget.noteContext !== noteContext) { - widget.setNoteContextEvent({ noteContext }); widget.activeContextChangedEvent({ noteContext }); } }