mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 17:18:53 +01:00
refactor(react): use memoization where appropriate
This commit is contained in:
parent
a6e56be55a
commit
3caefa5409
@ -1,4 +1,5 @@
|
|||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
|
import { memo } from "preact/compat";
|
||||||
import AbstractBulkAction from "./abstract_bulk_action";
|
import AbstractBulkAction from "./abstract_bulk_action";
|
||||||
|
|
||||||
interface BulkActionProps {
|
interface BulkActionProps {
|
||||||
@ -8,12 +9,17 @@ interface BulkActionProps {
|
|||||||
bulkAction: AbstractBulkAction;
|
bulkAction: AbstractBulkAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BulkAction({ label, children, helpText, bulkAction }: BulkActionProps) {
|
// Define styles as constants to prevent recreation
|
||||||
|
const flexContainerStyle = { display: "flex", alignItems: "center" } as const;
|
||||||
|
const labelStyle = { marginRight: "10px" } as const;
|
||||||
|
const textStyle = { marginRight: "10px", marginLeft: "10px" } as const;
|
||||||
|
|
||||||
|
const BulkAction = memo(({ label, children, helpText, bulkAction }: BulkActionProps) => {
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={2}>
|
<td colSpan={2}>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={flexContainerStyle}>
|
||||||
<div style={{ marginRight: "10px" }} className="text-nowrap">{label}</div>
|
<div style={labelStyle} className="text-nowrap">{label}</div>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@ -33,14 +39,16 @@ export default function BulkAction({ label, children, helpText, bulkAction }: Bu
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export function BulkActionText({ text }: { text: string }) {
|
export default BulkAction;
|
||||||
|
|
||||||
|
export const BulkActionText = memo(({ text }: { text: string }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ marginRight: "10px", marginLeft: "10px" }}
|
style={textStyle}
|
||||||
className="text-nowrap">
|
className="text-nowrap">
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import type { RefObject } from "preact";
|
import type { RefObject } from "preact";
|
||||||
import type { CSSProperties } from "preact/compat";
|
import type { CSSProperties } from "preact/compat";
|
||||||
import { useRef } from "preact/hooks";
|
import { useRef, useMemo } from "preact/hooks";
|
||||||
|
import { memo } from "preact/compat";
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
/** Reference to the button element. Mostly useful for requesting focus. */
|
/** Reference to the button element. Mostly useful for requesting focus. */
|
||||||
@ -17,26 +18,41 @@ interface ButtonProps {
|
|||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Button({ buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, small, style }: ButtonProps) {
|
const Button = memo(({ buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, small, style }: ButtonProps) => {
|
||||||
const classes: string[] = ["btn"];
|
// Memoize classes array to prevent recreation
|
||||||
|
const classes = useMemo(() => {
|
||||||
|
const classList: string[] = ["btn"];
|
||||||
if (primary) {
|
if (primary) {
|
||||||
classes.push("btn-primary");
|
classList.push("btn-primary");
|
||||||
} else {
|
} else {
|
||||||
classes.push("btn-secondary");
|
classList.push("btn-secondary");
|
||||||
}
|
}
|
||||||
if (className) {
|
if (className) {
|
||||||
classes.push(className);
|
classList.push(className);
|
||||||
}
|
}
|
||||||
if (small) {
|
if (small) {
|
||||||
classes.push("btn-sm");
|
classList.push("btn-sm");
|
||||||
}
|
}
|
||||||
|
return classList.join(" ");
|
||||||
|
}, [primary, className, small]);
|
||||||
|
|
||||||
const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null);
|
const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null);
|
||||||
const splitShortcut = (keyboardShortcut ?? "").split("+");
|
|
||||||
|
// Memoize keyboard shortcut rendering
|
||||||
|
const shortcutElements = useMemo(() => {
|
||||||
|
if (!keyboardShortcut) return null;
|
||||||
|
const splitShortcut = keyboardShortcut.split("+");
|
||||||
|
return splitShortcut.map((key, index) => (
|
||||||
|
<>
|
||||||
|
<kbd key={index}>{key.toUpperCase()}</kbd>
|
||||||
|
{index < splitShortcut.length - 1 ? "+" : ""}
|
||||||
|
</>
|
||||||
|
));
|
||||||
|
}, [keyboardShortcut]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classes.join(" ")}
|
className={classes}
|
||||||
type={onClick ? "button" : "submit"}
|
type={onClick ? "button" : "submit"}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
@ -44,13 +60,9 @@ export default function Button({ buttonRef: _buttonRef, className, text, onClick
|
|||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{icon && <span className={`bx ${icon}`}></span>}
|
{icon && <span className={`bx ${icon}`}></span>}
|
||||||
{text} {keyboardShortcut && (
|
{text} {shortcutElements}
|
||||||
splitShortcut.map((key, index) => (
|
|
||||||
<>
|
|
||||||
<kbd key={index}>{key.toUpperCase()}</kbd>{ index < splitShortcut.length - 1 ? "+" : "" }
|
|
||||||
</>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default Button;
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { Tooltip } from "bootstrap";
|
import { Tooltip } from "bootstrap";
|
||||||
import { useEffect, useRef } from "preact/hooks";
|
import { useEffect, useRef, useMemo, useCallback } from "preact/hooks";
|
||||||
import { escapeQuotes } from "../../services/utils";
|
import { escapeQuotes } from "../../services/utils";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
|
import { memo } from "preact/compat";
|
||||||
|
|
||||||
interface FormCheckboxProps {
|
interface FormCheckboxProps {
|
||||||
name: string;
|
name: string;
|
||||||
@ -15,28 +16,41 @@ interface FormCheckboxProps {
|
|||||||
onChange(newValue: boolean): void;
|
onChange(newValue: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FormCheckbox({ name, disabled, label, currentValue, onChange, hint }: FormCheckboxProps) {
|
const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint }: FormCheckboxProps) => {
|
||||||
const labelRef = useRef<HTMLLabelElement>(null);
|
const labelRef = useRef<HTMLLabelElement>(null);
|
||||||
|
|
||||||
if (hint) {
|
// Fix: Move useEffect outside conditional
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let tooltipInstance: Tooltip | null = null;
|
if (!hint || !labelRef.current) return;
|
||||||
if (labelRef.current) {
|
|
||||||
tooltipInstance = Tooltip.getOrCreateInstance(labelRef.current, {
|
const tooltipInstance = Tooltip.getOrCreateInstance(labelRef.current, {
|
||||||
html: true,
|
html: true,
|
||||||
template: '<div class="tooltip tooltip-top" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>'
|
template: '<div class="tooltip tooltip-top" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>'
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return () => tooltipInstance?.dispose();
|
return () => tooltipInstance?.dispose();
|
||||||
}, [labelRef.current]);
|
}, [hint]); // Proper dependency
|
||||||
}
|
|
||||||
|
// Memoize style object
|
||||||
|
const labelStyle = useMemo(() =>
|
||||||
|
hint ? { textDecoration: "underline dotted var(--main-text-color)" } : undefined,
|
||||||
|
[hint]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize onChange handler
|
||||||
|
const handleChange = useCallback((e: Event) => {
|
||||||
|
onChange((e.target as HTMLInputElement).checked);
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
// Memoize title attribute
|
||||||
|
const titleText = useMemo(() => hint ? escapeQuotes(hint) : undefined, [hint]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-checkbox">
|
<div className="form-checkbox">
|
||||||
<label
|
<label
|
||||||
className="form-check-label tn-checkbox"
|
className="form-check-label tn-checkbox"
|
||||||
style={hint && { textDecoration: "underline dotted var(--main-text-color)" }}
|
style={labelStyle}
|
||||||
title={hint && escapeQuotes(hint)}
|
title={titleText}
|
||||||
ref={labelRef}
|
ref={labelRef}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -46,9 +60,11 @@ export default function FormCheckbox({ name, disabled, label, currentValue, onCh
|
|||||||
checked={currentValue || false}
|
checked={currentValue || false}
|
||||||
value="1"
|
value="1"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={e => onChange((e.target as HTMLInputElement).checked)} />
|
onChange={handleChange} />
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default FormCheckbox;
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import { useContext, useEffect, useRef } from "preact/hooks";
|
import { useContext, useEffect, useRef, useMemo, useCallback } from "preact/hooks";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import type { CSSProperties, RefObject } from "preact/compat";
|
import type { CSSProperties, RefObject } from "preact/compat";
|
||||||
import { openDialog } from "../../services/dialog";
|
import { openDialog } from "../../services/dialog";
|
||||||
import { ParentComponent } from "./ReactBasicWidget";
|
import { ParentComponent } from "./ReactBasicWidget";
|
||||||
import { Modal as BootstrapModal } from "bootstrap";
|
import { Modal as BootstrapModal } from "bootstrap";
|
||||||
|
import { memo } from "preact/compat";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
className: string;
|
className: string;
|
||||||
@ -101,18 +102,25 @@ export default function Modal({ children, className, size, title, header, footer
|
|||||||
}
|
}
|
||||||
}, [ show ]);
|
}, [ show ]);
|
||||||
|
|
||||||
const dialogStyle: CSSProperties = {};
|
// Memoize styles to prevent recreation on every render
|
||||||
|
const dialogStyle = useMemo<CSSProperties>(() => {
|
||||||
|
const style: CSSProperties = {};
|
||||||
if (zIndex) {
|
if (zIndex) {
|
||||||
dialogStyle.zIndex = zIndex;
|
style.zIndex = zIndex;
|
||||||
}
|
}
|
||||||
|
return style;
|
||||||
|
}, [zIndex]);
|
||||||
|
|
||||||
const documentStyle: CSSProperties = {};
|
const documentStyle = useMemo<CSSProperties>(() => {
|
||||||
|
const style: CSSProperties = {};
|
||||||
if (maxWidth) {
|
if (maxWidth) {
|
||||||
documentStyle.maxWidth = `${maxWidth}px`;
|
style.maxWidth = `${maxWidth}px`;
|
||||||
}
|
}
|
||||||
if (minWidth) {
|
if (minWidth) {
|
||||||
documentStyle.minWidth = minWidth;
|
style.minWidth = minWidth;
|
||||||
}
|
}
|
||||||
|
return style;
|
||||||
|
}, [maxWidth, minWidth]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`modal fade mx-auto ${className}`} tabIndex={-1} style={dialogStyle} role="dialog" ref={modalRef}>
|
<div className={`modal fade mx-auto ${className}`} tabIndex={-1} style={dialogStyle} role="dialog" ref={modalRef}>
|
||||||
@ -132,10 +140,10 @@ export default function Modal({ children, className, size, title, header, footer
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{onSubmit ? (
|
{onSubmit ? (
|
||||||
<form ref={formRef} onSubmit={(e) => {
|
<form ref={formRef} onSubmit={useCallback((e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSubmit();
|
onSubmit();
|
||||||
}}>
|
}, [onSubmit])}>
|
||||||
<ModalInner footer={footer} bodyStyle={bodyStyle} footerStyle={footerStyle} footerAlignment={footerAlignment}>{children}</ModalInner>
|
<ModalInner footer={footer} bodyStyle={bodyStyle} footerStyle={footerStyle} footerAlignment={footerAlignment}>{children}</ModalInner>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
@ -149,11 +157,15 @@ export default function Modal({ children, className, size, title, header, footer
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModalInner({ children, footer, footerAlignment, bodyStyle, footerStyle: _footerStyle }: Pick<ModalProps, "children" | "footer" | "footerAlignment" | "bodyStyle" | "footerStyle">) {
|
const ModalInner = memo(({ children, footer, footerAlignment, bodyStyle, footerStyle: _footerStyle }: Pick<ModalProps, "children" | "footer" | "footerAlignment" | "bodyStyle" | "footerStyle">) => {
|
||||||
const footerStyle: CSSProperties = _footerStyle ?? {};
|
// Memoize footer style
|
||||||
|
const footerStyle = useMemo<CSSProperties>(() => {
|
||||||
|
const style: CSSProperties = _footerStyle ?? {};
|
||||||
if (footerAlignment === "between") {
|
if (footerAlignment === "between") {
|
||||||
footerStyle.justifyContent = "space-between";
|
style.justifyContent = "space-between";
|
||||||
}
|
}
|
||||||
|
return style;
|
||||||
|
}, [_footerStyle, footerAlignment]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -168,4 +180,4 @@ function ModalInner({ children, footer, footerAlignment, bodyStyle, footerStyle:
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user