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

This commit is contained in:
Elian Doran
2025-08-25 18:00:10 +03:00
parent e386b03b90
commit 733ec2c145
9 changed files with 125 additions and 162 deletions

View File

@@ -6,11 +6,6 @@ import { t } from "../../services/i18n";
export default function CallToActionDialog() { export default function CallToActionDialog() {
const activeCallToActions = useMemo(() => getCallToActions(), []); const activeCallToActions = useMemo(() => getCallToActions(), []);
if (!activeCallToActions.length) {
return <></>;
}
const [ activeIndex, setActiveIndex ] = useState(0); const [ activeIndex, setActiveIndex ] = useState(0);
const [ shown, setShown ] = useState(true); const [ shown, setShown ] = useState(true);
const activeItem = activeCallToActions[activeIndex]; const activeItem = activeCallToActions[activeIndex];
@@ -23,7 +18,7 @@ export default function CallToActionDialog() {
} }
} }
return ( return (activeCallToActions.length &&
<Modal <Modal
className="call-to-action" className="call-to-action"
size="md" size="md"

View File

@@ -36,7 +36,6 @@ export default function NoteTypeChooserDialogComponent() {
setShown(true); setShown(true);
}); });
if (!noteTypes.length) {
useEffect(() => { useEffect(() => {
note_types.getNoteTypeItems().then(noteTypes => { note_types.getNoteTypeItems().then(noteTypes => {
let index = -1; let index = -1;
@@ -53,8 +52,7 @@ export default function NoteTypeChooserDialogComponent() {
return item; return item;
})); }));
}); });
}); }, []);
}
function onNoteTypeSelected(value: string) { function onNoteTypeSelected(value: string) {
const [ noteType, templateNoteId ] = value.split(","); const [ noteType, templateNoteId ] = value.split(",");

View File

@@ -18,21 +18,15 @@ import { useTriliumEvent } from "../react/hooks";
export default function RecentChangesDialog() { export default function RecentChangesDialog() {
const [ ancestorNoteId, setAncestorNoteId ] = useState<string>(); const [ ancestorNoteId, setAncestorNoteId ] = useState<string>();
const [ groupedByDate, setGroupedByDate ] = useState<Map<string, RecentChangeRow[]>>(); const [ groupedByDate, setGroupedByDate ] = useState<Map<string, RecentChangeRow[]>>();
const [ needsRefresh, setNeedsRefresh ] = useState(false); const [ refreshCounter, setRefreshCounter ] = useState(0);
const [ shown, setShown ] = useState(false); const [ shown, setShown ] = useState(false);
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => { useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
setNeedsRefresh(true);
setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId()); setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId());
setShown(true); setShown(true);
}); });
if (!groupedByDate || needsRefresh) {
useEffect(() => { useEffect(() => {
if (needsRefresh) {
setNeedsRefresh(false);
}
server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`) server.get<RecentChangeRow[]>(`recent-changes/${ancestorNoteId}`)
.then(async (recentChanges) => { .then(async (recentChanges) => {
// preload all notes into cache // preload all notes into cache
@@ -44,8 +38,7 @@ export default function RecentChangesDialog() {
const groupedByDate = groupByDate(recentChanges); const groupedByDate = groupByDate(recentChanges);
setGroupedByDate(groupedByDate); setGroupedByDate(groupedByDate);
}); });
}) }, [ shown, refreshCounter ])
}
return ( return (
<Modal <Modal
@@ -60,7 +53,7 @@ export default function RecentChangesDialog() {
style={{ padding: "0 10px" }} style={{ padding: "0 10px" }}
onClick={() => { onClick={() => {
server.post("notes/erase-deleted-notes-now").then(() => { server.post("notes/erase-deleted-notes-now").then(() => {
setNeedsRefresh(true); setRefreshCounter(refreshCounter + 1);
toast.showMessage(t("recent_changes.deleted_notes_message")); toast.showMessage(t("recent_changes.deleted_notes_message"));
}); });
}} }}
@@ -113,10 +106,6 @@ function RecentChangesTimeline({ groupedByDate, setShown }: { groupedByDate: Map
} }
function NoteLink({ notePath, title }: { notePath: string, title: string }) { function NoteLink({ notePath, title }: { notePath: string, title: string }) {
if (!notePath || !title) {
return null;
}
const [ noteLink, setNoteLink ] = useState<JQuery<HTMLElement> | null>(null); const [ noteLink, setNoteLink ] = useState<JQuery<HTMLElement> | null>(null);
useEffect(() => { useEffect(() => {
link.createLink(notePath, { link.createLink(notePath, {

View File

@@ -201,17 +201,9 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi
return <></>; return <></>;
} }
switch (revisionItem.type) { switch (revisionItem.type) {
case "text": { case "text":
const contentRef = useRef<HTMLDivElement>(null); return <RevisionContentText content={content} />
useEffect(() => {
if (contentRef.current?.querySelector("span.math-tex")) {
renderMathInElement(contentRef.current, { trust: true });
}
});
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
}
case "code": case "code":
return <pre style={CODE_STYLE}>{content}</pre>; return <pre style={CODE_STYLE}>{content}</pre>;
case "image": case "image":
@@ -263,6 +255,16 @@ function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: Revisi
} }
} }
function RevisionContentText({ content }: { content: string | Buffer<ArrayBufferLike> | undefined }) {
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (contentRef.current?.querySelector("span.math-tex")) {
renderMathInElement(contentRef.current, { trust: true });
}
}, [content]);
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
}
function RevisionFooter({ note }: { note?: FNote }) { function RevisionFooter({ note }: { note?: FNote }) {
if (!note) { if (!note) {
return <></>; return <></>;

View File

@@ -23,12 +23,12 @@ export default function UploadAttachmentsDialog() {
setShown(true); setShown(true);
}); });
if (parentNoteId) {
useEffect(() => { useEffect(() => {
if (!parentNoteId) return;
tree.getNoteTitle(parentNoteId).then((noteTitle) => tree.getNoteTitle(parentNoteId).then((noteTitle) =>
setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle }))); setDescription(t("upload_attachments.files_will_be_uploaded", { noteTitle })));
}, [parentNoteId]); }, [parentNoteId]);
}
return ( return (
<Modal <Modal

View File

@@ -71,7 +71,6 @@ export default function Modal({ children, className, size, title, header, footer
const parentWidget = useContext(ParentComponent); const parentWidget = useContext(ParentComponent);
const elementToFocus = useRef<Element | null>(); const elementToFocus = useRef<Element | null>();
if (onShown || onHidden) {
useEffect(() => { useEffect(() => {
const modalElement = modalRef.current; const modalElement = modalRef.current;
if (!modalElement) { if (!modalElement) {
@@ -92,8 +91,7 @@ export default function Modal({ children, className, size, title, header, footer
} }
modalElement.removeEventListener("hidden.bs.modal", onHidden); modalElement.removeEventListener("hidden.bs.modal", onHidden);
}; };
}, [ ]); }, [ onShown, onHidden ]);
}
useEffect(() => { useEffect(() => {
if (!parentWidget) { if (!parentWidget) {

View File

@@ -216,15 +216,13 @@ export function useNoteContext() {
setNote(noteContext?.note); setNote(noteContext?.note);
}); });
useLegacyImperativeHandlers({ const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) { (parentComponent as ReactWrappedWidget & { setNoteContextEvent: (data: EventData<"setNoteContext">) => void }).setNoteContextEvent = ({ noteContext }: EventData<"setNoteContext">) => {
setNoteContext(noteContext); setNoteContext(noteContext);
} }
}, true);
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`); useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
return { return {
note: note, note: note,
@@ -249,25 +247,21 @@ export function useNoteContext() {
* @returns the value of the requested property. * @returns the value of the requested property.
*/ */
export function useNoteProperty<T extends keyof FNote>(note: FNote | null | undefined, property: T, componentId?: string) { export function useNoteProperty<T extends keyof FNote>(note: FNote | null | undefined, property: T, componentId?: string) {
if (!note) { const [, setValue ] = useState<FNote[T] | undefined>(note?.[property]);
return null; const refreshValue = () => setValue(note?.[property]);
}
const [, setValue ] = useState<FNote[T]>(note[property]);
const refreshValue = () => setValue(note[property]);
// Watch for note changes. // Watch for note changes.
useEffect(() => refreshValue(), [ note, note[property] ]); useEffect(() => refreshValue(), [ note, note?.[property] ]);
// Watch for external changes. // Watch for external changes.
useTriliumEvent("entitiesReloaded", ({ loadResults }) => { useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.isNoteReloaded(note.noteId, componentId)) { if (loadResults.isNoteReloaded(note?.noteId, componentId)) {
refreshValue(); refreshValue();
} }
}); });
useDebugValue(property); useDebugValue(property);
return note[property]; return note?.[property];
} }
export function useNoteRelation(note: FNote | undefined | null, relationName: string): [string | null | undefined, (newValue: string) => void] { export function useNoteRelation(note: FNote | undefined | null, relationName: string): [string | null | undefined, (newValue: string) => void] {
@@ -362,10 +356,6 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: s
} }
export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | undefined ] { export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | undefined ] {
if (!note) {
return [ undefined ];
}
const [ blob, setBlob ] = useState<FBlob | null>(); const [ blob, setBlob ] = useState<FBlob | null>();
function refresh() { function refresh() {
@@ -379,7 +369,7 @@ export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | un
} }
}); });
useDebugValue(note.noteId); useDebugValue(note?.noteId);
return [ blob ] as const; return [ blob ] as const;
} }
@@ -514,13 +504,9 @@ export function useTooltip(elRef: RefObject<HTMLElement>, config: Partial<Toolti
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export function useLegacyImperativeHandlers(handlers: Record<string, Function>, force?: boolean) { export function useLegacyImperativeHandlers(handlers: Record<string, Function>) {
const parentComponent = useContext(ParentComponent); const parentComponent = useContext(ParentComponent);
if (!force) {
useEffect(() => { useEffect(() => {
Object.assign(parentComponent as never, handlers); Object.assign(parentComponent as never, handlers);
}, [ handlers ]) }, [ handlers ]);
} else {
Object.assign(parentComponent as never, handlers);
}
} }

View File

@@ -20,16 +20,16 @@ interface NoteActionsProps {
noteContext?: NoteContext; noteContext?: NoteContext;
} }
export default function NoteActions(props: NoteActionsProps) { export default function NoteActions({ note, noteContext }: NoteActionsProps) {
return ( return (
<> <>
<RevisionsButton {...props} /> {note && <RevisionsButton note={note} />}
<NoteContextMenu {...props} /> {note && note.type !== "launcher" && <NoteContextMenu note={note as FNote} noteContext={noteContext}/>}
</> </>
); );
} }
function RevisionsButton({ note }: NoteActionsProps) { function RevisionsButton({ note }: { note: FNote }) {
const isEnabled = !["launcher", "doc"].includes(note?.type ?? ""); const isEnabled = !["launcher", "doc"].includes(note?.type ?? "");
return (isEnabled && return (isEnabled &&
@@ -42,12 +42,7 @@ function RevisionsButton({ note }: NoteActionsProps) {
); );
} }
function NoteContextMenu(props: NoteActionsProps) { function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
const { note, noteContext } = props;
if (!note || note.type === "launcher") {
return <></>;
}
const parentComponent = useContext(ParentComponent); const parentComponent = useContext(ParentComponent);
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment(); const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type); const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type);
@@ -65,7 +60,7 @@ function NoteContextMenu(props: NoteActionsProps) {
hideToggleArrow hideToggleArrow
noSelectButtonStyle noSelectButtonStyle
> >
{canBeConvertedToAttachment && <ConvertToAttachment {...props} /> } {canBeConvertedToAttachment && <ConvertToAttachment note={note} /> }
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")} />} {note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")} />}
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} /> <CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
<CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} /> <CommandItem command="printActiveNote" icon="bx bx-printer" disabled={!isPrintable} text={t("note_actions.print_note")} />
@@ -110,7 +105,7 @@ function CommandItem({ icon, text, title, command, disabled }: { icon: string, t
>{text}</FormListItem> >{text}</FormListItem>
} }
function ConvertToAttachment({ note }: NoteActionsProps) { function ConvertToAttachment({ note }: { note: FNote }) {
return ( return (
<FormListItem <FormListItem
icon="bx bx-paperclip" icon="bx bx-paperclip"

View File

@@ -77,10 +77,6 @@ export default function EtapiSettings() {
} }
function TokenList({ tokens }: { tokens: EtapiToken[] }) { function TokenList({ tokens }: { tokens: EtapiToken[] }) {
if (!tokens.length) {
return <div>{t("etapi.no_tokens_yet")}</div>;
}
const renameCallback = useCallback<RenameTokenCallback>(async (tokenId: string, oldName: string) => { const renameCallback = useCallback<RenameTokenCallback>(async (tokenId: string, oldName: string) => {
const tokenName = await dialog.prompt({ const tokenName = await dialog.prompt({
title: t("etapi.rename_token_title"), title: t("etapi.rename_token_title"),
@@ -104,6 +100,7 @@ function TokenList({ tokens }: { tokens: EtapiToken[] }) {
}, []); }, []);
return ( return (
tokens.length ? (
<div style={{ overflow: "auto", height: "500px"}}> <div style={{ overflow: "auto", height: "500px"}}>
<table className="table table-stripped"> <table className="table table-stripped">
<thead> <thead>
@@ -140,5 +137,8 @@ function TokenList({ tokens }: { tokens: EtapiToken[] }) {
</tbody> </tbody>
</table> </table>
</div> </div>
) : (
<div>{t("etapi.no_tokens_yet")}</div>
) )
);
} }