perf(codebase): implement in-memory cache for file tree to persist state across tab navigations

This commit is contained in:
2026-06-14 13:13:50 -07:00
parent 3774a1771b
commit 1895c8f947

View File

@@ -38,21 +38,50 @@ interface GiteaFileTreeProps {
selectedPath?: string; selectedPath?: string;
} }
// ── In-memory cache to persist tree state across tab navigations ──
const treeCache: Record<
string,
{
rootItems: TreeItem[];
childrenByPath: Record<string, TreeItem[]>;
expanded: Set<string>;
}
> = {};
export function GiteaFileTree({ export function GiteaFileTree({
projectId, projectId,
rootPath, rootPath,
onSelectFile, onSelectFile,
selectedPath, selectedPath,
}: GiteaFileTreeProps) { }: GiteaFileTreeProps) {
const [rootItems, setRootItems] = useState<TreeItem[] | null>(null); const cacheKey = `${projectId}::${rootPath}`;
const [loading, setLoading] = useState(true);
const [rootItems, setRootItems] = useState<TreeItem[] | null>(() => {
return treeCache[cacheKey]?.rootItems ?? null;
});
const [loading, setLoading] = useState(() => !treeCache[cacheKey]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [expanded, setExpanded] = useState<Set<string>>(new Set()); const [expanded, setExpanded] = useState<Set<string>>(() => {
return treeCache[cacheKey]?.expanded ?? new Set();
});
const [childrenByPath, setChildrenByPath] = useState< const [childrenByPath, setChildrenByPath] = useState<
Record<string, TreeItem[]> Record<string, TreeItem[]>
>({}); >(() => {
return treeCache[cacheKey]?.childrenByPath ?? {};
});
const [loadingPaths, setLoadingPaths] = useState<Set<string>>(new Set()); const [loadingPaths, setLoadingPaths] = useState<Set<string>>(new Set());
// Keep cache synced with state updates
useEffect(() => {
if (rootItems) {
treeCache[cacheKey] = {
rootItems,
childrenByPath,
expanded,
};
}
}, [cacheKey, rootItems, childrenByPath, expanded]);
const fetchPath = useCallback( const fetchPath = useCallback(
async (path: string): Promise<TreeItem[]> => { async (path: string): Promise<TreeItem[]> => {
const res = await fetch( const res = await fetch(
@@ -71,12 +100,12 @@ export function GiteaFileTree({
// Load root whenever projectId or rootPath changes // Load root whenever projectId or rootPath changes
useEffect(() => { useEffect(() => {
// If we already loaded this from cache on mount, skip fetching again
if (treeCache[cacheKey]) return;
let cancelled = false; let cancelled = false;
setLoading(true); setLoading(true);
setError(null); setError(null);
setRootItems(null);
setExpanded(new Set());
setChildrenByPath({});
fetchPath(rootPath) fetchPath(rootPath)
.then(async (items) => { .then(async (items) => {
@@ -119,7 +148,7 @@ export function GiteaFileTree({
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [projectId, rootPath, fetchPath]); }, [projectId, rootPath, fetchPath, cacheKey]);
const toggleDir = useCallback( const toggleDir = useCallback(
async (path: string) => { async (path: string) => {