From 518d2f4a0b4043bca2ae4e3ea75df57e2bc6e6ff Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Mon, 1 Dec 2025 18:15:32 -0800 Subject: [PATCH] Fix hook deps and memoize ListView core row model --- .../src/components/Explorer/context.tsx | 18 ++++--- .../Explorer/views/ListView/ListView.tsx | 50 ++++++++++++------- .../Explorer/views/ListView/useTable.tsx | 9 +++- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/packages/interface/src/components/Explorer/context.tsx b/packages/interface/src/components/Explorer/context.tsx index 872ed6a47..7098e2634 100644 --- a/packages/interface/src/components/Explorer/context.tsx +++ b/packages/interface/src/components/Explorer/context.tsx @@ -135,7 +135,8 @@ export function ExplorerProvider({ children, spaceItemId: initialSpaceItemId }: setViewModeInternal(prefs.viewMode); setViewSettingsInternal(prefs.viewSettings); } - }, [spaceItemKey, viewPrefs]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [spaceItemKey]); // Load sort preferences when path changes useEffect(() => { @@ -143,19 +144,22 @@ export function ExplorerProvider({ children, spaceItemId: initialSpaceItemId }: if (sortPref) { setSortByInternal(sortPref as DirectorySortBy | MediaSortBy); } - }, [pathKey, sortPrefs]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathKey]); // Wrapper for setViewMode that persists to store const setViewMode = useCallback((mode: "grid" | "list" | "media" | "column" | "size" | "knowledge") => { setViewModeInternal(mode); viewPrefs.setPreferences(spaceItemKey, { viewMode: mode }); - }, [spaceItemKey, viewPrefs]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [spaceItemKey]); // Wrapper for setSortBy that persists to store const setSortBy = useCallback((sort: DirectorySortBy | MediaSortBy) => { setSortByInternal(sort); sortPrefs.setPreferences(pathKey, sort); - }, [pathKey, sortPrefs]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathKey]); // Update sort when switching to media view useEffect(() => { @@ -166,7 +170,8 @@ export function ExplorerProvider({ children, spaceItemId: initialSpaceItemId }: setSortByInternal("modified"); sortPrefs.setPreferences(pathKey, "modified"); } - }, [viewMode, sortByInternal, pathKey, sortPrefs]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [viewMode, sortByInternal, pathKey]); const setViewSettings = useCallback((settings: Partial) => { setViewSettingsInternal((prev) => { @@ -174,7 +179,8 @@ export function ExplorerProvider({ children, spaceItemId: initialSpaceItemId }: viewPrefs.setPreferences(spaceItemKey, { viewSettings: updated }); return updated; }); - }, [spaceItemKey, viewPrefs]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [spaceItemKey]); const devicesQuery = useLibraryQuery({ type: "devices.list", diff --git a/packages/interface/src/components/Explorer/views/ListView/ListView.tsx b/packages/interface/src/components/Explorer/views/ListView/ListView.tsx index 9f1776c50..ed55ce473 100644 --- a/packages/interface/src/components/Explorer/views/ListView/ListView.tsx +++ b/packages/interface/src/components/Explorer/views/ListView/ListView.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useEffect, memo } from "react"; +import { useCallback, useRef, useEffect, memo, useMemo } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { flexRender } from "@tanstack/react-table"; import { CaretDown } from "@phosphor-icons/react"; @@ -26,19 +26,28 @@ export const ListView = memo(function ListView() { const headerScrollRef = useRef(null); const bodyScrollRef = useRef(null); + // Memoize query input to prevent unnecessary re-fetches + const queryInput = useMemo( + () => + currentPath + ? { + path: currentPath, + limit: null, + include_hidden: false, + sort_by: sortBy as DirectorySortBy, + } + : null!, + [currentPath, sortBy] + ); + + const pathScope = useMemo(() => currentPath ?? undefined, [currentPath]); + const directoryQuery = useNormalizedCache({ wireMethod: "query:files.directory_listing", - input: currentPath - ? { - path: currentPath, - limit: null, - include_hidden: false, - sort_by: sortBy as DirectorySortBy, - } - : null!, + input: queryInput, resourceType: "file", enabled: !!currentPath, - pathScope: currentPath ?? undefined, + pathScope, }); const files = directoryQuery.data?.files || []; @@ -64,37 +73,44 @@ export const ListView = memo(function ListView() { } }, []); - // Keyboard navigation + // Store values in refs to avoid effect re-runs + const rowVirtualizerRef = useRef(rowVirtualizer); + rowVirtualizerRef.current = rowVirtualizer; + const filesRef = useRef(files); + filesRef.current = files; + + // Keyboard navigation - stable effect, uses refs for changing values useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); const direction = e.key === "ArrowDown" ? "down" : "up"; + const currentFiles = filesRef.current; const currentIndex = focusedIndex >= 0 ? focusedIndex : 0; const newIndex = direction === "down" - ? Math.min(currentIndex + 1, files.length - 1) + ? Math.min(currentIndex + 1, currentFiles.length - 1) : Math.max(currentIndex - 1, 0); if (e.shiftKey) { // Range selection with shift - if (newIndex !== focusedIndex && files[newIndex]) { - selectFile(files[newIndex], files, false, true); + if (newIndex !== focusedIndex && currentFiles[newIndex]) { + selectFile(currentFiles[newIndex], currentFiles, false, true); setFocusedIndex(newIndex); } } else { - moveFocus(direction, files); + moveFocus(direction, currentFiles); } // Scroll to keep selection visible - rowVirtualizer.scrollToIndex(newIndex, { align: "auto" }); + rowVirtualizerRef.current.scrollToIndex(newIndex, { align: "auto" }); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [focusedIndex, files, selectFile, setFocusedIndex, moveFocus, rowVirtualizer]); + }, [focusedIndex, selectFile, setFocusedIndex, moveFocus]); // Column sorting handler const handleHeaderClick = useCallback( diff --git a/packages/interface/src/components/Explorer/views/ListView/useTable.tsx b/packages/interface/src/components/Explorer/views/ListView/useTable.tsx index 0b45076d9..9469e7dba 100644 --- a/packages/interface/src/components/Explorer/views/ListView/useTable.tsx +++ b/packages/interface/src/components/Explorer/views/ListView/useTable.tsx @@ -16,6 +16,9 @@ export const TABLE_HEADER_HEIGHT = 32; // Column definitions for the list view export function useTable(files: File[]) { + // Memoize files array reference to prevent unnecessary table updates + const stableFiles = useMemo(() => files, [JSON.stringify(files.map(f => f.id))]); + const columns = useMemo[]>( () => [ { @@ -55,14 +58,16 @@ export function useTable(files: File[]) { [] ); + const coreRowModel = useMemo(() => getCoreRowModel(), []); + const table = useReactTable({ - data: files, + data: stableFiles, columns, defaultColumn: { minSize: 60, maxSize: 500, }, - getCoreRowModel: getCoreRowModel(), + getCoreRowModel: coreRowModel, columnResizeMode: "onChange", getRowId: (row) => row.id, });