From f8e70c2c393a188f27712346a920f801f1425a8d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 28 Apr 2026 22:06:09 -0400 Subject: [PATCH] chore: resolve mechanical react-hooks/exhaustive-deps warnings (#12207) Adds missing stable dependencies (e.g. `t`, prop callbacks, store refs, `setFocusedCommentId`) and removes unnecessary ones across hooks where the fix is straightforward. For the two MobX-observed `.orderedData` deps in `History.tsx`, keeps the original deps and silences the false positive with `eslint-disable-next-line` so the memos still recompute when the underlying observable arrays change. Co-authored-by: Claude Opus 4.7 --- app/components/Collaborators.tsx | 9 ++++++++- app/components/Editor.tsx | 4 ++-- .../Sharing/components/ShareSubscribeForm.tsx | 2 +- app/components/Sidebar/Sidebar.tsx | 4 ++-- .../Sidebar/components/CollectionLinkChildren.tsx | 4 ++-- .../Sidebar/components/SharedWithMeLink.tsx | 2 +- app/components/Sidebar/components/SidebarLink.tsx | 2 +- app/editor/components/LinkEditor.tsx | 2 +- app/editor/components/MediaDimension.tsx | 11 ++++++++++- app/editor/components/MediaLinkEditor.tsx | 6 +++--- app/editor/components/MentionMenu.tsx | 2 +- app/editor/components/SuggestionsMenu.tsx | 2 +- app/hooks/useCollectionMenuAction.tsx | 2 +- .../components/Comments/CommentThreadItem.tsx | 2 +- app/scenes/Document/components/DataLoader.tsx | 1 + app/scenes/Document/components/Editor.tsx | 4 ++-- app/scenes/Document/components/History/History.tsx | 8 +++++--- app/scenes/DocumentDelete.tsx | 11 ++++++++++- app/scenes/Settings/components/GroupMembersTable.tsx | 1 + app/scenes/Settings/components/SharesTable.tsx | 2 +- plugins/notion/client/Imports.tsx | 10 +++++++++- plugins/notion/client/components/ImportDialog.tsx | 10 +++++++++- shared/editor/components/Mentions.tsx | 2 +- 23 files changed, 74 insertions(+), 29 deletions(-) diff --git a/app/components/Collaborators.tsx b/app/components/Collaborators.tsx index 958e8134ff..f67e15dda4 100644 --- a/app/components/Collaborators.tsx +++ b/app/components/Collaborators.tsx @@ -146,7 +146,14 @@ function Collaborators(props: Props) { /> ); }, - [presentIds, editingIds, observingUserId, currentUserId, handleAvatarClick] + [ + presentIds, + editingIds, + observingUserId, + currentUserId, + handleAvatarClick, + t, + ] ); if (!document.insightsEnabled) { diff --git a/app/components/Editor.tsx b/app/components/Editor.tsx index a2175ac392..ad14209ce0 100644 --- a/app/components/Editor.tsx +++ b/app/components/Editor.tsx @@ -99,7 +99,7 @@ function Editor(props: Props, ref: React.RefObject | null) { ); }, 2000); onFileUploadStart?.(); - }, [onFileUploadStart, dictionary.uploadingWithProgress]); + }, [onFileUploadStart, dictionary]); const handleFileUploadProgress = React.useCallback( (fileId: string, fractionComplete: number) => { @@ -118,7 +118,7 @@ function Editor(props: Props, ref: React.RefObject | null) { }); } }, - [dictionary.uploadingWithProgress] + [dictionary] ); const handleFileUploadStop = React.useCallback(() => { diff --git a/app/components/Sharing/components/ShareSubscribeForm.tsx b/app/components/Sharing/components/ShareSubscribeForm.tsx index 529e1c968a..860d3d134b 100644 --- a/app/components/Sharing/components/ShareSubscribeForm.tsx +++ b/app/components/Sharing/components/ShareSubscribeForm.tsx @@ -40,7 +40,7 @@ export function ShareSubscribeForm({ setStatus("error"); } }, - [shareId, documentId, email] + [shareId, documentId, email, t] ); const handleChange = useCallback( diff --git a/app/components/Sidebar/Sidebar.tsx b/app/components/Sidebar/Sidebar.tsx index 79251b7b92..33a0836e49 100644 --- a/app/components/Sidebar/Sidebar.tsx +++ b/app/components/Sidebar/Sidebar.tsx @@ -83,7 +83,7 @@ const Sidebar = React.forwardRef(function Sidebar_( ui.set({ sidebarWidth: Math.max(newWidth, minWidth) }); } }, - [ui, theme, offset, minWidth, maxWidth, direction] + [ui, theme, offset, minWidth, maxWidth, direction, canCollapse] ); const handleStopDrag = React.useCallback(() => { @@ -107,7 +107,7 @@ const Sidebar = React.forwardRef(function Sidebar_( } else { ui.set({ sidebarWidth: width }); } - }, [ui, isSmallerThanMinimum, minWidth, width]); + }, [ui, isSmallerThanMinimum, minWidth, width, canCollapse]); const handleBlur = React.useCallback(() => { setHovering(false); diff --git a/app/components/Sidebar/components/CollectionLinkChildren.tsx b/app/components/Sidebar/components/CollectionLinkChildren.tsx index ecd865297a..5fa0cd647b 100644 --- a/app/components/Sidebar/components/CollectionLinkChildren.tsx +++ b/app/components/Sidebar/components/CollectionLinkChildren.tsx @@ -49,13 +49,13 @@ function CollectionLinkChildren({ if (!expanded) { setShowing(pageSize); } - }, [expanded]); + }, [expanded, pageSize]); const showMore = useCallback(() => { if (childDocuments && childDocuments.length > showing) { setShowing((value) => value + pageSize); } - }, [childDocuments, showing]); + }, [childDocuments, showing, pageSize]); return ( diff --git a/app/components/Sidebar/components/SharedWithMeLink.tsx b/app/components/Sidebar/components/SharedWithMeLink.tsx index 771cc1ec53..87f5cb02a6 100644 --- a/app/components/Sidebar/components/SharedWithMeLink.tsx +++ b/app/components/Sidebar/components/SharedWithMeLink.tsx @@ -70,7 +70,7 @@ function SharedWithMeLink({ membership, depth = 0 }: Props) { void documents.fetch(documentId); void membership.fetchDocuments(); } - }, [documentId, documents]); + }, [documentId, documents, membership]); React.useEffect(() => { if (isActiveDocument && membership.documentId) { diff --git a/app/components/Sidebar/components/SidebarLink.tsx b/app/components/Sidebar/components/SidebarLink.tsx index cbe01f9814..2381c57c27 100644 --- a/app/components/Sidebar/components/SidebarLink.tsx +++ b/app/components/Sidebar/components/SidebarLink.tsx @@ -132,7 +132,7 @@ function SidebarLink( onClick(ev); } }, - [onClick, disabled, expanded] + [onClick, disabled] ); const handleDisclosureClick = React.useCallback( diff --git a/app/editor/components/LinkEditor.tsx b/app/editor/components/LinkEditor.tsx index 906a39c806..cbfadd4232 100644 --- a/app/editor/components/LinkEditor.tsx +++ b/app/editor/components/LinkEditor.tsx @@ -102,7 +102,7 @@ const LinkEditor: React.FC = ({ const openLink = React.useCallback(() => { commands["openLink"](); - }, []); + }, [commands]); const removeLink = React.useCallback(() => { commands["removeLink"](); diff --git a/app/editor/components/MediaDimension.tsx b/app/editor/components/MediaDimension.tsx index 706f85907d..1c22cc4ae6 100644 --- a/app/editor/components/MediaDimension.tsx +++ b/app/editor/components/MediaDimension.tsx @@ -166,7 +166,16 @@ export function MediaDimension() { height: finalHeight, }); } - }, [commands, width, height, localDimension, nodeType, error, reset]); + }, [ + commands, + width, + height, + localDimension, + nodeType, + error, + reset, + isOutsideBounds, + ]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { diff --git a/app/editor/components/MediaLinkEditor.tsx b/app/editor/components/MediaLinkEditor.tsx index bdc9228b62..248626f229 100644 --- a/app/editor/components/MediaLinkEditor.tsx +++ b/app/editor/components/MediaLinkEditor.tsx @@ -61,7 +61,7 @@ export function MediaLinkEditor({ const { state, dispatch } = view; dispatch(state.tr.deleteSelection()); onLinkRemove(); - }, [view]); + }, [view, onLinkRemove]); const update = useCallback(() => { const { state } = view; @@ -74,7 +74,7 @@ export function MediaLinkEditor({ view.dispatch(tr); moveSelectionToEnd(); onLinkUpdate(); - }, [localUrl, node, view, moveSelectionToEnd]); + }, [localUrl, node, view, moveSelectionToEnd, onLinkUpdate]); useOnClickOutside(wrapperRef, onClickOutside); @@ -99,7 +99,7 @@ export function MediaLinkEditor({ } } }, - [update, moveSelectionToEnd] + [update, moveSelectionToEnd, onEscape] ); if (!node) { diff --git a/app/editor/components/MentionMenu.tsx b/app/editor/components/MentionMenu.tsx index 3b27fcc660..f8026bdf8e 100644 --- a/app/editor/components/MentionMenu.tsx +++ b/app/editor/components/MentionMenu.tsx @@ -69,7 +69,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) { res.data.collections.map(collections.add); res.data.groups.map(groups.add); }); - }, [search, documents, users, collections]) + }, [search, documents, users, collections, groups, maxResultsInSection]) ); useEffect(() => { diff --git a/app/editor/components/SuggestionsMenu.tsx b/app/editor/components/SuggestionsMenu.tsx index 2f812cc642..31b8de8014 100644 --- a/app/editor/components/SuggestionsMenu.tsx +++ b/app/editor/components/SuggestionsMenu.tsx @@ -187,7 +187,7 @@ function SuggestionsMenu(props: Props) { selection.to ) ); - }, [props.search, props.trigger, view]); + }, [props.search, props.trigger, view, isMobile]); const restoreSelection = React.useCallback(() => { if (!isMobile) { diff --git a/app/hooks/useCollectionMenuAction.tsx b/app/hooks/useCollectionMenuAction.tsx index 71cea2cdf4..7cc5ffef03 100644 --- a/app/hooks/useCollectionMenuAction.tsx +++ b/app/hooks/useCollectionMenuAction.tsx @@ -65,7 +65,7 @@ export function useCollectionMenuAction({ collectionId, onRename }: Props) { ActionSeparator, deleteCollection, ], - [t, can.createDocument, can.update, onRename] + [t, can.update, onRename] ); return useMenuAction(actions); diff --git a/app/scenes/Document/components/Comments/CommentThreadItem.tsx b/app/scenes/Document/components/Comments/CommentThreadItem.tsx index 49d93ecd05..b7bd4f7d90 100644 --- a/app/scenes/Document/components/Comments/CommentThreadItem.tsx +++ b/app/scenes/Document/components/Comments/CommentThreadItem.tsx @@ -158,7 +158,7 @@ function CommentThreadItem({ setFocusedCommentId(null); } }, - [comment.id, onUpdate] + [comment.id, onUpdate, setFocusedCommentId] ); const handleDelete = React.useCallback(() => { diff --git a/app/scenes/Document/components/DataLoader.tsx b/app/scenes/Document/components/DataLoader.tsx index 49523ea78b..a2a33960e5 100644 --- a/app/scenes/Document/components/DataLoader.tsx +++ b/app/scenes/Document/components/DataLoader.tsx @@ -222,6 +222,7 @@ function DataLoader({ match, children }: Props) { shares, ui, revisionId, + missingPolicy, ]); // Auto-enter presentation mode when ?present=true query param is set diff --git a/app/scenes/Document/components/Editor.tsx b/app/scenes/Document/components/Editor.tsx index c8e3d1a296..251bc93106 100644 --- a/app/scenes/Document/components/Editor.tsx +++ b/app/scenes/Document/components/Editor.tsx @@ -103,7 +103,7 @@ function DocumentEditor(props: Props, ref: React.RefObject) { } ui.set({ rightSidebar: "comments" }); } - }, [focusedComment, ui, document.id, params]); + }, [focusedComment, ui, document.id, params, setFocusedCommentId]); // Save document when blurring title, but delay so that if clicking on a // button this is allowed to execute first. @@ -148,7 +148,7 @@ function DocumentEditor(props: Props, ref: React.RefObject) { setFocusedCommentId(commentId); } }, - [comments, user?.id, props.id] + [comments, user?.id, props.id, setFocusedCommentId] ); // Soft delete the Comment model when associated mark is totally removed. diff --git a/app/scenes/Document/components/History/History.tsx b/app/scenes/Document/components/History/History.tsx index 48730e7ad1..6dd9aa3186 100644 --- a/app/scenes/Document/components/History/History.tsx +++ b/app/scenes/Document/components/History/History.tsx @@ -96,7 +96,7 @@ function History() { updateLocation({ changes: null, compareTo: null }); } }, - [updateLocation] + [updateLocation, setDefaultShowChanges] ); const selectedRevisionId = historyMatch?.params.revisionId; @@ -127,7 +127,7 @@ function History() { if (defaultShowChanges) { updateLocation({ changes: "true" }); } - }, [defaultShowChanges]); + }, [defaultShowChanges, updateLocation]); const fetchHistory = React.useCallback(async () => { if (!document) { @@ -170,6 +170,7 @@ function History() { .getByDocumentId(document.id) .filter((revision: Revision) => revision.id !== latestRevisionId) .slice(0, revisionsOffset); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [document, revisions.orderedData, revisionsOffset]); const nonRevisionEvents = React.useMemo( @@ -177,6 +178,7 @@ function History() { document ? events.getByDocumentId(document.id).slice(0, eventsOffset) : [], + // eslint-disable-next-line react-hooks/exhaustive-deps [document, events.orderedData, eventsOffset] ); @@ -228,7 +230,7 @@ function History() { } else { history.goBack(); } - }, [history, document, sidebarContext]); + }, [history, document, sidebarContext, isMobile]); useKeyDown("Escape", onCloseHistory); diff --git a/app/scenes/DocumentDelete.tsx b/app/scenes/DocumentDelete.tsx index c0bb730c39..76e370f1cc 100644 --- a/app/scenes/DocumentDelete.tsx +++ b/app/scenes/DocumentDelete.tsx @@ -69,7 +69,16 @@ function DocumentDelete({ document, onSubmit }: Props) { setDeleting(false); } }, - [onSubmit, ui, document, documents, history, collection] + [ + onSubmit, + ui, + document, + documents, + history, + collection, + userMemberships, + groupMemberships, + ] ); const handleArchive = React.useCallback( diff --git a/app/scenes/Settings/components/GroupMembersTable.tsx b/app/scenes/Settings/components/GroupMembersTable.tsx index 33ee685c35..4c0630919d 100644 --- a/app/scenes/Settings/components/GroupMembersTable.tsx +++ b/app/scenes/Settings/components/GroupMembersTable.tsx @@ -177,6 +177,7 @@ export const GroupMembersTable = observer(function GroupMembersTable({ t, can.update, group.id, + group.isExternallyManaged, groupUsers.orderedData, permissions, handlePermissionChange, diff --git a/app/scenes/Settings/components/SharesTable.tsx b/app/scenes/Settings/components/SharesTable.tsx index bede112a6c..5657eb7be5 100644 --- a/app/scenes/Settings/components/SharesTable.tsx +++ b/app/scenes/Settings/components/SharesTable.tsx @@ -146,7 +146,7 @@ export function SharesTable({ data, canManage, ...rest }: Props) { } : undefined, ]), - [t, hasDomain, canManage] + [t, hasDomain, canManage, formatNumber] ); return ( diff --git a/plugins/notion/client/Imports.tsx b/plugins/notion/client/Imports.tsx index 8d5766aa91..af7f3b8853 100644 --- a/plugins/notion/client/Imports.tsx +++ b/plugins/notion/client/Imports.tsx @@ -55,7 +55,15 @@ export const Notion = observer(() => { onClose: clearQueryParams, }); } - }, [t, dialogs, oauthSuccess, service, clearQueryParams]); + }, [ + t, + dialogs, + oauthSuccess, + service, + clearQueryParams, + handleSubmit, + integrationId, + ]); React.useEffect(() => { if (!oauthError) { diff --git a/plugins/notion/client/components/ImportDialog.tsx b/plugins/notion/client/components/ImportDialog.tsx index f8e892b0cf..64d3562e9d 100644 --- a/plugins/notion/client/components/ImportDialog.tsx +++ b/plugins/notion/client/components/ImportDialog.tsx @@ -53,7 +53,15 @@ export function ImportDialog({ integrationId, onSubmit }: Props) { toast.error(err.message); resetSubmitting(); } - }, [permission, onSubmit]); + }, [ + permission, + onSubmit, + integrationId, + t, + imports, + resetSubmitting, + setSubmitting, + ]); return ( diff --git a/shared/editor/components/Mentions.tsx b/shared/editor/components/Mentions.tsx index 59e04feead..597d18ff2d 100644 --- a/shared/editor/components/Mentions.tsx +++ b/shared/editor/components/Mentions.tsx @@ -239,7 +239,7 @@ export const MentionURL = (props: IssueUrlProps) => { }; void fetchUnfurl(); - }, [unfurls, url, node, isMounted]); + }, [unfurls, url, node, isMounted, onChangeUnfurl]); if (!unfurl) { return !loaded ? (