fix(react/settings): hook leak after closing tabs

This commit is contained in:
Elian Doran 2025-08-18 22:15:47 +03:00
parent 3837466cb3
commit 73ff41f2b2
No known key found for this signature in database
5 changed files with 51 additions and 8 deletions

View File

@ -2,6 +2,7 @@ import FlexContainer from "./flex_container.js";
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js";
import type NoteContext from "../../components/note_context.js";
import Component from "../../components/component.js";
interface NoteContextEvent {
noteContext: NoteContext;
@ -152,6 +153,8 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
for (const ntxId of ntxIds) {
this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove();
const widget = this.widgets[ntxId];
recursiveCleanup(widget);
delete this.widgets[ntxId];
}
}
@ -237,3 +240,12 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
return Promise.all(promises);
}
}
function recursiveCleanup(widget: Component) {
for (const child of widget.children) {
recursiveCleanup(child);
}
if ("cleanup" in widget && typeof widget.cleanup === "function") {
widget.cleanup();
}
}

View File

@ -22,11 +22,18 @@ export default abstract class ReactBasicWidget extends BasicWidget {
* @returns the rendered wrapped DOM element.
*/
export function renderReactWidget(parentComponent: Component, el: JSX.Element) {
const renderContainer = new DocumentFragment();
return renderReactWidgetAtElement(parentComponent, el, new DocumentFragment()).children();
}
export function renderReactWidgetAtElement(parentComponent: Component, el: JSX.Element, container: Element | DocumentFragment) {
render((
<ParentComponent.Provider value={parentComponent}>
{el}
</ParentComponent.Provider>
), renderContainer);
return $(renderContainer.children) as JQuery<HTMLElement>;
), container);
return $(container) as JQuery<HTMLElement>;
}
export function disposeReactWidget(container: Element) {
render(null, container);
}

View File

@ -70,7 +70,15 @@ export default function useTriliumEvent<T extends EventNames>(eventName: T, hand
// Remove the event handler from the array.
const newEventHandlers = eventHandlers.filter(e => e !== handler);
registeredHandlers.get(parentWidget)?.set(eventName, newEventHandlers);
if (newEventHandlers.length) {
registeredHandlers.get(parentWidget)?.set(eventName, newEventHandlers);
} else {
registeredHandlers.get(parentWidget)?.delete(eventName);
}
if (!registeredHandlers.get(parentWidget)?.size) {
registeredHandlers.delete(parentWidget);
}
};
}, [ eventName, parentWidget, handler ]);
}

View File

@ -7,7 +7,7 @@ import { t } from "../../services/i18n.js";
import type BasicWidget from "../basic_widget.js";
import type { JSX } from "preact/jsx-runtime";
import AppearanceSettings from "./options/appearance.jsx";
import { renderReactWidget } from "../react/ReactBasicWidget.jsx";
import { disposeReactWidget, renderReactWidget, renderReactWidgetAtElement } from "../react/ReactBasicWidget.jsx";
import ImageSettings from "./options/images.jsx";
import AdvancedSettings from "./options/advanced.jsx";
import InternationalizationOptions from "./options/i18n.jsx";
@ -21,6 +21,7 @@ import TextNoteSettings from "./options/text_notes.jsx";
import CodeNoteSettings from "./options/code_notes.jsx";
import OtherSettings from "./options/other.jsx";
import BackendLogWidget from "./content/backend_log.js";
import { unmountComponentAtNode } from "preact/compat";
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
<style>
@ -120,7 +121,18 @@ export default class ContentWidgetTypeWidget extends TypeWidget {
}
// React widget.
this.$content.append(renderReactWidget(this, contentWidgets));
renderReactWidgetAtElement(this, contentWidgets, this.$content[0]);
}
cleanup(): void {
if (this.noteId) {
const contentWidgets = (CONTENT_WIDGETS as Record<string, (typeof NoteContextAwareWidget[] | JSX.Element)>)[this.noteId];
if (contentWidgets && !Array.isArray(contentWidgets)) {
disposeReactWidget(this.$content[0]);
}
}
super.cleanup();
}
}

View File

@ -101,4 +101,8 @@ export default abstract class TypeWidget extends NoteContextAwareWidget {
return Promise.resolve();
}
}
cleanup(): void {
super.cleanup();
}
}