refactor(react): fix a few more rules of hooks violations

This commit is contained in:
Elian Doran 2025-08-25 18:41:48 +03:00
parent 733ec2c145
commit 5a54dd666f
No known key found for this signature in database
6 changed files with 27 additions and 18 deletions

View file

@ -1,6 +1,6 @@
import type { RefObject } from "preact";
import type { CSSProperties } from "preact/compat";
import { useRef, useMemo } from "preact/hooks";
import { useMemo } from "preact/hooks";
import { memo } from "preact/compat";
import { CommandNames } from "../../components/app_context";
@ -22,7 +22,7 @@ export interface ButtonProps {
title?: string;
}
const Button = memo(({ name, buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, size, style, triggerCommand, ...restProps }: ButtonProps) => {
const Button = memo(({ name, buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, size, style, triggerCommand, ...restProps }: ButtonProps) => {
// Memoize classes array to prevent recreation
const classes = useMemo(() => {
const classList: string[] = ["btn"];
@ -42,8 +42,6 @@ const Button = memo(({ name, buttonRef: _buttonRef, className, text, onClick, ke
return classList.join(" ");
}, [primary, className, size]);
const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null);
// Memoize keyboard shortcut rendering
const shortcutElements = useMemo(() => {
if (!keyboardShortcut) return null;

View file

@ -6,8 +6,7 @@ import { CSSProperties, memo } from "preact/compat";
import { useUniqueName } from "./hooks";
interface FormCheckboxProps {
id?: string;
name?: string;
name: string;
label: string | ComponentChildren;
/**
* If set, the checkbox label will be underlined and dotted, indicating a hint. When hovered, it will show the hint text.
@ -19,9 +18,9 @@ interface FormCheckboxProps {
containerStyle?: CSSProperties;
}
const FormCheckbox = memo(({ name, id: _id, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
const id = _id ?? useUniqueName(name);
const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
const labelRef = useRef<HTMLLabelElement>(null);
const id = useUniqueName(name);
// Fix: Move useEffect outside conditional
useEffect(() => {

View file

@ -1,4 +1,4 @@
import { useContext, useEffect, useRef, useMemo, useCallback } from "preact/hooks";
import { useContext, useEffect, useRef, useMemo } from "preact/hooks";
import { t } from "../../services/i18n";
import { ComponentChildren } from "preact";
import type { CSSProperties, RefObject } from "preact/compat";
@ -6,6 +6,7 @@ import { openDialog } from "../../services/dialog";
import { ParentComponent } from "./react_utils";
import { Modal as BootstrapModal } from "bootstrap";
import { memo } from "preact/compat";
import { useSyncedRef } from "./hooks";
interface ModalProps {
className: string;
@ -64,10 +65,9 @@ interface ModalProps {
stackable?: boolean;
}
export default function Modal({ children, className, size, title, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: _modalRef, formRef: _formRef, bodyStyle, show, stackable }: ModalProps) {
const modalRef = _modalRef ?? useRef<HTMLDivElement>(null);
export default function Modal({ children, className, size, title, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable }: ModalProps) {
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
const modalInstanceRef = useRef<BootstrapModal>();
const formRef = _formRef ?? useRef<HTMLFormElement>(null);
const parentWidget = useContext(ParentComponent);
const elementToFocus = useRef<Element | null>();
@ -145,10 +145,10 @@ export default function Modal({ children, className, size, title, header, footer
</div>
{onSubmit ? (
<form ref={formRef} onSubmit={useCallback((e) => {
<form ref={formRef} onSubmit={(e) => {
e.preventDefault();
onSubmit();
}, [onSubmit])}>
}}>
<ModalInner footer={footer} bodyStyle={bodyStyle} footerStyle={footerStyle} footerAlignment={footerAlignment}>{children}</ModalInner>
</form>
) : (

View file

@ -1,9 +1,9 @@
import { useRef } from "preact/hooks";
import { t } from "../../services/i18n";
import { useEffect } from "preact/hooks";
import note_autocomplete, { Options, type Suggestion } from "../../services/note_autocomplete";
import type { RefObject } from "preact";
import type { CSSProperties } from "preact/compat";
import { useSyncedRef } from "./hooks";
interface NoteAutocompleteProps {
id?: string;
@ -19,8 +19,8 @@ interface NoteAutocompleteProps {
noteId?: string;
}
export default function NoteAutocomplete({ id, inputRef: _ref, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) {
const ref = _ref ?? useRef<HTMLInputElement>(null);
export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) {
const ref = useSyncedRef<HTMLInputElement>(externalInputRef);
useEffect(() => {
if (!ref.current) return;

View file

@ -509,4 +509,16 @@ export function useLegacyImperativeHandlers(handlers: Record<string, Function>)
useEffect(() => {
Object.assign(parentComponent as never, handlers);
}, [ handlers ]);
}
export function useSyncedRef<T>(externalRef?: RefObject<T>, initialValue: T | null = null): RefObject<T> {
const ref = useRef<T>(initialValue);
useEffect(() => {
if (externalRef) {
externalRef.current = ref.current;
}
}, [ ref, externalRef ]);
return ref;
}

View file

@ -132,7 +132,7 @@ interface SingleProviderSettingsProps {
}
function SingleProviderSettings({ provider, title, apiKeyDescription, baseUrlDescription, modelDescription, validationErrorMessage, apiKeyOption, baseUrlOption, modelOption }: SingleProviderSettingsProps) {
const [ apiKey, setApiKey ] = apiKeyOption ? useTriliumOption(apiKeyOption) : [];
const [ apiKey, setApiKey ] = useTriliumOption(apiKeyOption ?? baseUrlOption);
const [ baseUrl, setBaseUrl ] = useTriliumOption(baseUrlOption);
const isValid = (apiKeyOption ? !!apiKey : !!baseUrl);