fix(react/settings): useTriliumEvent not cleaning up properly

This commit is contained in:
Elian Doran 2025-08-14 21:05:24 +03:00
parent e7b448e2bc
commit 2793df06c4
No known key found for this signature in database
2 changed files with 58 additions and 26 deletions

View File

@ -5,7 +5,10 @@ import SpacedUpdate from "../../services/spaced_update";
import { OptionNames } from "@triliumnext/commons"; import { OptionNames } from "@triliumnext/commons";
import options from "../../services/options"; import options from "../../services/options";
import utils, { reloadFrontendApp } from "../../services/utils"; import utils, { reloadFrontendApp } from "../../services/utils";
import { __values } from "tslib"; import Component from "../../components/component";
type TriliumEventHandler<T extends EventNames> = (data: EventData<T>) => void;
const registeredHandlers: Map<Component, Map<EventNames, TriliumEventHandler<any>[]>> = new Map();
/** /**
* Allows a React component to react to Trilium events (e.g. `entitiesReloaded`). When the desired event is triggered, the handler is invoked with the event parameters. * Allows a React component to react to Trilium events (e.g. `entitiesReloaded`). When the desired event is triggered, the handler is invoked with the event parameters.
@ -16,32 +19,59 @@ import { __values } from "tslib";
* @param handler the handler to be invoked when the event is triggered. * @param handler the handler to be invoked when the event is triggered.
* @param enabled determines whether the event should be listened to or not. Useful to conditionally limit the listener based on a state (e.g. a modal being displayed). * @param enabled determines whether the event should be listened to or not. Useful to conditionally limit the listener based on a state (e.g. a modal being displayed).
*/ */
export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void, enabled = true) { export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: TriliumEventHandler<T>, enabled = true) {
const parentWidget = useContext(ParentComponent); const parentWidget = useContext(ParentComponent);
useEffect(() => { if (!parentWidget) {
if (!parentWidget || !enabled) { return;
return; }
}
const handlerName = `${eventName}Event`;
// Create a unique handler name for this specific event listener const customHandler = useMemo(() => {
const handlerName = `${eventName}Event`; return async (data: EventData<T>) => {
const originalHandler = parentWidget[handlerName]; // Inform the attached event listeners.
const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName) ?? [];
// Override the event handler to call our handler for (const eventHandler of eventHandlers) {
parentWidget[handlerName] = async function(data: EventData<T>) { eventHandler(data);
// Call original handler if it exists
if (originalHandler) {
await originalHandler.call(parentWidget, data);
} }
// Call our React component's handler }
handler(data); }, [ eventName, parentWidget ]);
};
// Cleanup: restore original handler on unmount or when disabled useEffect(() => {
// Attach to the list of handlers.
let handlersByWidget = registeredHandlers.get(parentWidget);
if (!handlersByWidget) {
handlersByWidget = new Map();
registeredHandlers.set(parentWidget, handlersByWidget);
}
let handlersByWidgetAndEventName = handlersByWidget.get(eventName);
if (!handlersByWidgetAndEventName) {
handlersByWidgetAndEventName = [];
handlersByWidget.set(eventName, handlersByWidgetAndEventName);
}
if (!handlersByWidgetAndEventName.includes(handler)) {
handlersByWidgetAndEventName.push(handler);
}
// Apply the custom event handler.
if (parentWidget[handlerName] && parentWidget[handlerName] !== customHandler) {
console.warn(`Widget ${parentWidget.componentId} already had an event listener and it was replaced by the React one.`);
}
parentWidget[handlerName] = customHandler;
return () => { return () => {
parentWidget[handlerName] = originalHandler; const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName);
if (!eventHandlers || !eventHandlers.includes(handler)) {
return;
}
// Remove the event handler from the array.
const newEventHandlers = eventHandlers.filter(e => e !== handler);
registeredHandlers.get(parentWidget)?.set(eventName, newEventHandlers);
}; };
}, [parentWidget, enabled, eventName, handler]); }, [ eventName, parentWidget, handler ]);
} }
export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000) { export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000) {

View File

@ -74,12 +74,14 @@ const FONT_FAMILIES: FontGroup[] = [
]; ];
export default function AppearanceSettings() { export default function AppearanceSettings() {
const [ overrideThemeFonts ] = useTriliumOption("overrideThemeFonts");
return ( return (
<> <div>
<LayoutOrientation /> <LayoutOrientation />
<ApplicationTheme /> <ApplicationTheme />
<Fonts /> {overrideThemeFonts === "true" && <Fonts />}
</> </div>
) )
} }
@ -141,7 +143,7 @@ function ApplicationTheme() {
) )
} }
function Fonts() { function Fonts() {
return ( return (
<OptionsSection title={t("fonts.fonts")}> <OptionsSection title={t("fonts.fonts")}>
<Font title={t("fonts.main_font")} fontFamilyOption="mainFontFamily" /> <Font title={t("fonts.main_font")} fontFamilyOption="mainFontFamily" />