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}
+
@@ -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 ? (
-
) : (
@@ -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