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

View file

@ -6,8 +6,7 @@ import { CSSProperties, memo } from "preact/compat";
import { useUniqueName } from "./hooks"; import { useUniqueName } from "./hooks";
interface FormCheckboxProps { interface FormCheckboxProps {
id?: string; name: string;
name?: string;
label: string | ComponentChildren; label: string | ComponentChildren;
/** /**
* If set, the checkbox label will be underlined and dotted, indicating a hint. When hovered, it will show the hint text. * 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; containerStyle?: CSSProperties;
} }
const FormCheckbox = memo(({ name, id: _id, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => { const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
const id = _id ?? useUniqueName(name);
const labelRef = useRef<HTMLLabelElement>(null); const labelRef = useRef<HTMLLabelElement>(null);
const id = useUniqueName(name);
// Fix: Move useEffect outside conditional // Fix: Move useEffect outside conditional
useEffect(() => { 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 { 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";
@ -6,6 +6,7 @@ import { openDialog } from "../../services/dialog";
import { ParentComponent } from "./react_utils"; import { ParentComponent } from "./react_utils";
import { Modal as BootstrapModal } from "bootstrap"; import { Modal as BootstrapModal } from "bootstrap";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
import { useSyncedRef } from "./hooks";
interface ModalProps { interface ModalProps {
className: string; className: string;
@ -64,10 +65,9 @@ interface ModalProps {
stackable?: boolean; 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) { 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 = _modalRef ?? useRef<HTMLDivElement>(null); const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
const modalInstanceRef = useRef<BootstrapModal>(); const modalInstanceRef = useRef<BootstrapModal>();
const formRef = _formRef ?? useRef<HTMLFormElement>(null);
const parentWidget = useContext(ParentComponent); const parentWidget = useContext(ParentComponent);
const elementToFocus = useRef<Element | null>(); const elementToFocus = useRef<Element | null>();
@ -145,10 +145,10 @@ export default function Modal({ children, className, size, title, header, footer
</div> </div>
{onSubmit ? ( {onSubmit ? (
<form ref={formRef} onSubmit={useCallback((e) => { <form ref={formRef} onSubmit={(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>
) : ( ) : (

View file

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

View file

@ -510,3 +510,15 @@ export function useLegacyImperativeHandlers(handlers: Record<string, Function>)
Object.assign(parentComponent as never, handlers); Object.assign(parentComponent as never, handlers);
}, [ 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) { 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 [ baseUrl, setBaseUrl ] = useTriliumOption(baseUrlOption);
const isValid = (apiKeyOption ? !!apiKey : !!baseUrl); const isValid = (apiKeyOption ? !!apiKey : !!baseUrl);