diff --git a/apps/client/src/widgets/bulk_actions/BulkAction.tsx b/apps/client/src/widgets/bulk_actions/BulkAction.tsx index 1f9595971..3389d59f5 100644 --- a/apps/client/src/widgets/bulk_actions/BulkAction.tsx +++ b/apps/client/src/widgets/bulk_actions/BulkAction.tsx @@ -1,4 +1,5 @@ import { ComponentChildren } from "preact"; +import { memo } from "preact/compat"; import AbstractBulkAction from "./abstract_bulk_action"; interface BulkActionProps { @@ -8,12 +9,17 @@ interface BulkActionProps { 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 ( -
-
{label}
+
+
{label}
{children}
@@ -33,14 +39,16 @@ export default function BulkAction({ label, children, helpText, bulkAction }: Bu ); -} +}); -export function BulkActionText({ text }: { text: string }) { +export default BulkAction; + +export const BulkActionText = memo(({ text }: { text: string }) => { return (
{text}
); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/apps/client/src/widgets/react/Button.tsx b/apps/client/src/widgets/react/Button.tsx index 4362b1680..5262cae66 100644 --- a/apps/client/src/widgets/react/Button.tsx +++ b/apps/client/src/widgets/react/Button.tsx @@ -1,6 +1,7 @@ import type { RefObject } from "preact"; import type { CSSProperties } from "preact/compat"; -import { useRef } from "preact/hooks"; +import { useRef, useMemo } from "preact/hooks"; +import { memo } from "preact/compat"; interface ButtonProps { /** Reference to the button element. Mostly useful for requesting focus. */ @@ -17,26 +18,41 @@ interface ButtonProps { style?: CSSProperties; } -export default function Button({ buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, small, style }: ButtonProps) { - const classes: string[] = ["btn"]; - if (primary) { - classes.push("btn-primary"); - } else { - classes.push("btn-secondary"); - } - if (className) { - classes.push(className); - } - if (small) { - classes.push("btn-sm"); - } +const Button = memo(({ buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, small, style }: ButtonProps) => { + // Memoize classes array to prevent recreation + const classes = useMemo(() => { + const classList: string[] = ["btn"]; + if (primary) { + classList.push("btn-primary"); + } else { + classList.push("btn-secondary"); + } + if (className) { + classList.push(className); + } + if (small) { + classList.push("btn-sm"); + } + return classList.join(" "); + }, [primary, className, small]); const buttonRef = _buttonRef ?? useRef(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) => ( + <> + {key.toUpperCase()} + {index < splitShortcut.length - 1 ? "+" : ""} + + )); + }, [keyboardShortcut]); return ( ); -} \ No newline at end of file +}); + +export default Button; \ No newline at end of file diff --git a/apps/client/src/widgets/react/FormCheckbox.tsx b/apps/client/src/widgets/react/FormCheckbox.tsx index 65c90c53f..4d6c2a384 100644 --- a/apps/client/src/widgets/react/FormCheckbox.tsx +++ b/apps/client/src/widgets/react/FormCheckbox.tsx @@ -1,7 +1,8 @@ import { Tooltip } from "bootstrap"; -import { useEffect, useRef } from "preact/hooks"; +import { useEffect, useRef, useMemo, useCallback } from "preact/hooks"; import { escapeQuotes } from "../../services/utils"; import { ComponentChildren } from "preact"; +import { memo } from "preact/compat"; interface FormCheckboxProps { name: string; @@ -15,28 +16,41 @@ interface FormCheckboxProps { 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(null); - if (hint) { - useEffect(() => { - let tooltipInstance: Tooltip | null = null; - if (labelRef.current) { - tooltipInstance = Tooltip.getOrCreateInstance(labelRef.current, { - html: true, - template: '' - }); - } - return () => tooltipInstance?.dispose(); - }, [labelRef.current]); - } + // Fix: Move useEffect outside conditional + useEffect(() => { + if (!hint || !labelRef.current) return; + + const tooltipInstance = Tooltip.getOrCreateInstance(labelRef.current, { + html: true, + template: '' + }); + + return () => tooltipInstance?.dispose(); + }, [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 (
); -} \ No newline at end of file +}); + +export default FormCheckbox; \ No newline at end of file diff --git a/apps/client/src/widgets/react/Modal.tsx b/apps/client/src/widgets/react/Modal.tsx index 84f20a9b4..024077f7b 100644 --- a/apps/client/src/widgets/react/Modal.tsx +++ b/apps/client/src/widgets/react/Modal.tsx @@ -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 { ComponentChildren } from "preact"; import type { CSSProperties, RefObject } from "preact/compat"; import { openDialog } from "../../services/dialog"; import { ParentComponent } from "./ReactBasicWidget"; import { Modal as BootstrapModal } from "bootstrap"; +import { memo } from "preact/compat"; interface ModalProps { className: string; @@ -101,18 +102,25 @@ export default function Modal({ children, className, size, title, header, footer } }, [ show ]); - const dialogStyle: CSSProperties = {}; - if (zIndex) { - dialogStyle.zIndex = zIndex; - } + // Memoize styles to prevent recreation on every render + const dialogStyle = useMemo(() => { + const style: CSSProperties = {}; + if (zIndex) { + style.zIndex = zIndex; + } + return style; + }, [zIndex]); - const documentStyle: CSSProperties = {}; - if (maxWidth) { - documentStyle.maxWidth = `${maxWidth}px`; - } - if (minWidth) { - documentStyle.minWidth = minWidth; - } + const documentStyle = useMemo(() => { + const style: CSSProperties = {}; + if (maxWidth) { + style.maxWidth = `${maxWidth}px`; + } + if (minWidth) { + style.minWidth = minWidth; + } + return style; + }, [maxWidth, minWidth]); return (
@@ -132,10 +140,10 @@ export default function Modal({ children, className, size, title, header, footer
{onSubmit ? ( -
{ + { e.preventDefault(); onSubmit(); - }}> + }, [onSubmit])}> {children}
) : ( @@ -149,11 +157,15 @@ export default function Modal({ children, className, size, title, header, footer ); } -function ModalInner({ children, footer, footerAlignment, bodyStyle, footerStyle: _footerStyle }: Pick) { - const footerStyle: CSSProperties = _footerStyle ?? {}; - if (footerAlignment === "between") { - footerStyle.justifyContent = "space-between"; - } +const ModalInner = memo(({ children, footer, footerAlignment, bodyStyle, footerStyle: _footerStyle }: Pick) => { + // Memoize footer style + const footerStyle = useMemo(() => { + const style: CSSProperties = _footerStyle ?? {}; + if (footerAlignment === "between") { + style.justifyContent = "space-between"; + } + return style; + }, [_footerStyle, footerAlignment]); return ( <> @@ -168,4 +180,4 @@ function ModalInner({ children, footer, footerAlignment, bodyStyle, footerStyle: )} ); -} \ No newline at end of file +}); \ No newline at end of file