Fix hook deps and memoize ListView core row model

This commit is contained in:
Jamie Pine
2025-12-01 18:15:32 -08:00
parent 9becfa1e7d
commit 518d2f4a0b
3 changed files with 52 additions and 25 deletions

View File

@@ -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<ViewSettings>) => {
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",

View File

@@ -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<HTMLDivElement>(null);
const bodyScrollRef = useRef<HTMLDivElement>(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(

View File

@@ -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<ColumnDef<File>[]>(
() => [
{
@@ -55,14 +58,16 @@ export function useTable(files: File[]) {
[]
);
const coreRowModel = useMemo(() => getCoreRowModel<File>(), []);
const table = useReactTable({
data: files,
data: stableFiles,
columns,
defaultColumn: {
minSize: 60,
maxSize: 500,
},
getCoreRowModel: getCoreRowModel(),
getCoreRowModel: coreRowModel,
columnResizeMode: "onChange",
getRowId: (row) => row.id,
});