feat(component): add removeChild method for cleanup of child components

feat(hooks): improve useLegacyWidget cleanup and memoization logic
This commit is contained in:
lzinga 2025-12-30 13:59:46 -08:00
parent 64a518a00b
commit 267a37d3bd
2 changed files with 29 additions and 8 deletions

View File

@ -57,6 +57,18 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
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<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | 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);

View File

@ -634,7 +634,8 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
const ref = useRef<HTMLDivElement>(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<T extends BasicWidget>(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<T extends BasicWidget>(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 ]);