refactor(doclist): remove loading state and unify navigation prefetch behavior

Eliminate unused loading state and related effects from document tile and list
row components. Standardize navigation by disabling prefetch on all document
open links in doclist views for consistent client-side routing. Cache
document list state on load to improve state restoration and initialization.
This commit is contained in:
Richard R 2026-05-28 09:15:18 -06:00
parent eff25ad8df
commit b9490a53ac
5 changed files with 42 additions and 39 deletions

View file

@ -36,6 +36,8 @@ import { ListView } from './views/ListView';
import { ColumnsView } from './views/ColumnsView'; import { ColumnsView } from './views/ColumnsView';
import { GalleryView } from './views/GalleryView'; import { GalleryView } from './views/GalleryView';
let cachedDocumentListState: DocumentListState | null = null;
type DocumentToDelete = { type DocumentToDelete = {
id: string; id: string;
name: string; name: string;
@ -116,22 +118,25 @@ interface DocumentListInnerProps {
} }
function DocumentListInner({ brand, appActions }: DocumentListInnerProps) { function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
const [sortBy, setSortBy] = useState<SortBy>(DEFAULT_STATE.sortBy); const cachedState = cachedDocumentListState;
const [sortBy, setSortBy] = useState<SortBy>(cachedState?.sortBy ?? DEFAULT_STATE.sortBy);
const [sortDirection, setSortDirection] = useState<SortDirection>( const [sortDirection, setSortDirection] = useState<SortDirection>(
DEFAULT_STATE.sortDirection, cachedState?.sortDirection ?? DEFAULT_STATE.sortDirection,
); );
const [viewMode, setViewMode] = useState<ViewMode>(DEFAULT_STATE.viewMode); const [viewMode, setViewMode] = useState<ViewMode>(
const [iconSize, setIconSize] = useState<IconSize>(DEFAULT_STATE.iconSize); normalizeViewMode(cachedState?.viewMode ?? DEFAULT_STATE.viewMode),
const [folders, setFolders] = useState<Folder[]>(DEFAULT_STATE.folders); );
const [showHint, setShowHint] = useState(true); const [iconSize, setIconSize] = useState<IconSize>(cachedState?.iconSize ?? DEFAULT_STATE.iconSize);
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_STATE.sidebarWidth); const [folders, setFolders] = useState<Folder[]>(cachedState?.folders ?? DEFAULT_STATE.folders);
const [sidebarFilter, setSidebarFilter] = useState<SidebarFilter>('all'); const [showHint, setShowHint] = useState(cachedState?.showHint ?? true);
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarWidth, setSidebarWidth] = useState(cachedState?.sidebarWidth ?? DEFAULT_STATE.sidebarWidth);
const [sidebarFilter, setSidebarFilter] = useState<SidebarFilter>(cachedState?.sidebarFilter ?? 'all');
const [sidebarOpen, setSidebarOpen] = useState(!(cachedState?.sidebarCollapsed ?? false));
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [recentlyOpenedById, setRecentlyOpenedById] = useState<Record<string, number>>({}); const [recentlyOpenedById, setRecentlyOpenedById] = useState<Record<string, number>>({});
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(cachedState !== null);
const [documentToDelete, setDocumentToDelete] = useState<DocumentToDelete | null>(null); const [documentToDelete, setDocumentToDelete] = useState<DocumentToDelete | null>(null);
const [pendingMerge, setPendingMerge] = useState< const [pendingMerge, setPendingMerge] = useState<
@ -164,6 +169,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
const saved = await getDocumentListState(); const saved = await getDocumentListState();
if (cancelled) return; if (cancelled) return;
if (saved) { if (saved) {
cachedDocumentListState = saved;
setSortBy(saved.sortBy); setSortBy(saved.sortBy);
setSortDirection(saved.sortDirection); setSortDirection(saved.sortDirection);
setFolders(saved.folders ?? []); setFolders(saved.folders ?? []);
@ -173,6 +179,17 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
setSidebarWidth(saved.sidebarWidth ?? DEFAULT_STATE.sidebarWidth); setSidebarWidth(saved.sidebarWidth ?? DEFAULT_STATE.sidebarWidth);
setSidebarFilter(saved.sidebarFilter ?? 'all'); setSidebarFilter(saved.sidebarFilter ?? 'all');
setSidebarOpen(!(saved.sidebarCollapsed ?? false)); setSidebarOpen(!(saved.sidebarCollapsed ?? false));
} else {
cachedDocumentListState = null;
setSortBy(DEFAULT_STATE.sortBy);
setSortDirection(DEFAULT_STATE.sortDirection);
setFolders(DEFAULT_STATE.folders);
setShowHint(DEFAULT_STATE.showHint);
setViewMode(DEFAULT_STATE.viewMode);
setIconSize(DEFAULT_STATE.iconSize);
setSidebarWidth(DEFAULT_STATE.sidebarWidth);
setSidebarFilter(DEFAULT_STATE.sidebarFilter);
setSidebarOpen(!DEFAULT_STATE.sidebarCollapsed);
} }
setIsInitialized(true); setIsInitialized(true);
})(); })();
@ -196,6 +213,7 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
sidebarFilter, sidebarFilter,
sidebarCollapsed: !sidebarOpen, sidebarCollapsed: !sidebarOpen,
}; };
cachedDocumentListState = state;
void saveDocumentListState(state); void saveDocumentListState(state);
}, [ }, [
sortBy, sortBy,

View file

@ -102,6 +102,9 @@ export function ColumnsView({
}: ColumnsViewProps) { }: ColumnsViewProps) {
const { setVisibleOrder } = useDocumentSelection(); const { setVisibleOrder } = useDocumentSelection();
const [selectedDoc, setSelectedDoc] = useState<DocumentListDocument | null>(null); const [selectedDoc, setSelectedDoc] = useState<DocumentListDocument | null>(null);
const openHref = selectedDoc
? `/${selectedDoc.type}/${encodeURIComponent(selectedDoc.id)}`
: null;
useEffect(() => { useEffect(() => {
setVisibleOrder(documents); setVisibleOrder(documents);
@ -154,7 +157,8 @@ export function ColumnsView({
</p> </p>
<div className="mt-3 flex gap-2"> <div className="mt-3 flex gap-2">
<Link <Link
href={`/${selectedDoc.type}/${encodeURIComponent(selectedDoc.id)}`} href={openHref || '/app'}
prefetch={false}
className="flex-1 inline-flex items-center justify-center h-8 rounded-md bg-accent text-background text-[12px] font-medium hover:bg-secondary-accent hover:scale-[1.01] transition-all duration-200 ease-out" className="flex-1 inline-flex items-center justify-center h-8 rounded-md bg-accent text-background text-[12px] font-medium hover:bg-secondary-accent hover:scale-[1.01] transition-all duration-200 ease-out"
> >
Open Open

View file

@ -1,7 +1,6 @@
'use client'; 'use client';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useDrag, useDrop, type DragSourceMonitor } from 'react-dnd'; import { useDrag, useDrop, type DragSourceMonitor } from 'react-dnd';
import { Button } from '@headlessui/react'; import { Button } from '@headlessui/react';
import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons'; import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons';
@ -70,7 +69,6 @@ export function DocumentTile({
onDelete, onDelete,
onMergeIntoFolder, onMergeIntoFolder,
}: DocumentTileProps) { }: DocumentTileProps) {
const [loading, setLoading] = useState(false);
const { authEnabled } = useAuthConfig(); const { authEnabled } = useAuthConfig();
const { data: session } = useAuthSession(); const { data: session } = useAuthSession();
const href = `/${doc.type}/${encodeURIComponent(doc.id)}`; const href = `/${doc.type}/${encodeURIComponent(doc.id)}`;
@ -136,19 +134,9 @@ export function DocumentTile({
if (e.shiftKey || e.metaKey || e.ctrlKey) { if (e.shiftKey || e.metaKey || e.ctrlKey) {
e.preventDefault(); e.preventDefault();
selection.select(doc, { shift: e.shiftKey, meta: e.metaKey || e.ctrlKey }); selection.select(doc, { shift: e.shiftKey, meta: e.metaKey || e.ctrlKey });
return;
} }
selection.replace([doc]);
setLoading(true);
}; };
// Cap any stuck loading flag if the user navigates back.
useEffect(() => {
if (!loading) return;
const t = setTimeout(() => setLoading(false), 2500);
return () => clearTimeout(t);
}, [loading]);
return ( return (
<div <div
ref={setRefs} ref={setRefs}
@ -160,12 +148,12 @@ export function DocumentTile({
? 'border-accent bg-offbase' ? 'border-accent bg-offbase'
: 'border-offbase bg-base hover:bg-offbase hover:border-accent') + : 'border-offbase bg-base hover:bg-offbase hover:border-accent') +
(isDropTarget ? ' ring-1 ring-accent' : '') + (isDropTarget ? ' ring-1 ring-accent' : '') +
(isDragging ? ' opacity-50' : '') + (isDragging ? ' opacity-50' : '')
(loading ? ' prism-outline' : '')
} }
> >
<Link <Link
href={href} href={href}
prefetch={false}
draggable={false} draggable={false}
className="block" className="block"
aria-label={`Open ${doc.name}`} aria-label={`Open ${doc.name}`}
@ -176,6 +164,7 @@ export function DocumentTile({
<div className={`flex items-center w-full ${BOTTOM_PADDING_CLASSES[iconSize]}`}> <div className={`flex items-center w-full ${BOTTOM_PADDING_CLASSES[iconSize]}`}>
<Link <Link
href={href} href={href}
prefetch={false}
draggable={false} draggable={false}
className={`flex items-center flex-1 min-w-0 rounded-md ${LINK_PADDING_CLASS} ${GAP_CLASSES[iconSize]}`} className={`flex items-center flex-1 min-w-0 rounded-md ${LINK_PADDING_CLASS} ${GAP_CLASSES[iconSize]}`}
onClick={handleClick} onClick={handleClick}

View file

@ -114,6 +114,7 @@ export function GalleryView({
const railRef = useRef<HTMLDivElement | null>(null); const railRef = useRef<HTMLDivElement | null>(null);
const [activeIdx, setActiveIdx] = useState(0); const [activeIdx, setActiveIdx] = useState(0);
const activeDoc = useMemo(() => documents[activeIdx], [documents, activeIdx]); const activeDoc = useMemo(() => documents[activeIdx], [documents, activeIdx]);
const openHref = activeDoc ? `/${activeDoc.type}/${encodeURIComponent(activeDoc.id)}` : null;
useEffect(() => { useEffect(() => {
setVisibleOrder(documents); setVisibleOrder(documents);
@ -163,7 +164,8 @@ export function GalleryView({
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Link <Link
href={`/${activeDoc.type}/${encodeURIComponent(activeDoc.id)}`} href={openHref || '/app'}
prefetch={false}
className="h-8 px-4 inline-flex items-center justify-center rounded-md bg-accent text-background text-[12px] font-medium hover:bg-secondary-accent hover:scale-[1.01] transition-all duration-200 ease-out" className="h-8 px-4 inline-flex items-center justify-center rounded-md bg-accent text-background text-[12px] font-medium hover:bg-secondary-accent hover:scale-[1.01] transition-all duration-200 ease-out"
> >
Open Open

View file

@ -1,7 +1,7 @@
'use client'; 'use client';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect } from 'react';
import { useDrag, useDrop } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd';
import { Button } from '@headlessui/react'; import { Button } from '@headlessui/react';
import type { import type {
@ -87,7 +87,6 @@ function DocRow({
const selection = useDocumentSelection(); const selection = useDocumentSelection();
const isSelected = selection.isSelected(doc); const isSelected = selection.isSelected(doc);
const isInFolder = Boolean(doc.folderId); const isInFolder = Boolean(doc.folderId);
const [loading, setLoading] = useState(false);
const href = `/${doc.type}/${encodeURIComponent(doc.id)}`; const href = `/${doc.type}/${encodeURIComponent(doc.id)}`;
const [{ isDragging }, dragRef] = useDrag<DocumentDragItem, void, { isDragging: boolean }>(() => ({ const [{ isDragging }, dragRef] = useDrag<DocumentDragItem, void, { isDragging: boolean }>(() => ({
@ -117,22 +116,13 @@ function DocRow({
dropRef(node); dropRef(node);
}; };
useEffect(() => {
if (!loading) return;
const t = setTimeout(() => setLoading(false), 2500);
return () => clearTimeout(t);
}, [loading]);
const isTarget = isOver && canDrop; const isTarget = isOver && canDrop;
const handleClick: React.MouseEventHandler = (e) => { const handleClick: React.MouseEventHandler = (e) => {
if (e.shiftKey || e.metaKey || e.ctrlKey) { if (e.shiftKey || e.metaKey || e.ctrlKey) {
e.preventDefault(); e.preventDefault();
selection.select(doc, { shift: e.shiftKey, meta: e.metaKey || e.ctrlKey }); selection.select(doc, { shift: e.shiftKey, meta: e.metaKey || e.ctrlKey });
return;
} }
selection.replace([doc]);
setLoading(true);
}; };
return ( return (
@ -146,12 +136,12 @@ function DocRow({
? 'bg-offbase text-accent' ? 'bg-offbase text-accent'
: 'text-foreground hover:bg-offbase') + : 'text-foreground hover:bg-offbase') +
(isTarget ? ' ring-1 ring-accent ring-inset' : '') + (isTarget ? ' ring-1 ring-accent ring-inset' : '') +
(isDragging ? ' opacity-50' : '') + (isDragging ? ' opacity-50' : '')
(loading ? ' prism-outline' : '')
} }
> >
<Link <Link
href={href} href={href}
prefetch={false}
draggable={false} draggable={false}
onClick={handleClick} onClick={handleClick}
className="flex items-center gap-2 min-w-0 px-2 py-1.5" className="flex items-center gap-2 min-w-0 px-2 py-1.5"