mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 04:16:17 +01:00 
			
		
		
		
	refactor(react): use memoization where appropriate
This commit is contained in:
		@@ -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:
 | 
				
			|||||||
            )}
 | 
					            )}
 | 
				
			||||||
        </>
 | 
					        </>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
}
 | 
					});
 | 
				
			||||||
		Reference in New Issue
	
	Block a user