Skip to content

Commit ed2fb58

Browse files
committed
feat: Improve data fetching in the library page
1 parent f937633 commit ed2fb58

File tree

6 files changed

+248
-206
lines changed

6 files changed

+248
-206
lines changed

src/api/chrisapiclient.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,51 @@ import Client from "@fnndsc/chrisapi";
22
import { Cookies } from "react-cookie";
33

44
/**
5-
* This is a singleton to hold an instantiated, authenticated `Client` object,
5+
* This is a singleton to hold an instantiated, authenticated Client object,
66
* in order to prevent every component that needs the client from having to be
77
* passed the token, declare process.env variables, etc.
88
*/
99

10-
// biome-ignore lint/complexity/noStaticOnlyClass: Singleton pattern
10+
// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
1111
class ChrisAPIClient {
12-
private static client: Client;
13-
private static isTokenAuthorized: boolean;
12+
private static client: Client | null = null;
13+
private static lastCreatedWith: string | null = null; // Track what token was used
1414

15+
/**
16+
* Get the ChRIS API client singleton instance
17+
* Optimized to avoid unnecessary recreation
18+
*/
1519
static getClient(): Client {
1620
const cookie = new Cookies();
17-
if (!ChrisAPIClient.client || !ChrisAPIClient.isTokenAuthorized) {
18-
const user = cookie.get("username");
19-
const token: string = cookie.get(`${user}_token`);
20-
if (token) {
21-
ChrisAPIClient.isTokenAuthorized = true;
22-
} else {
23-
ChrisAPIClient.isTokenAuthorized = false;
24-
}
25-
ChrisAPIClient.client = new Client(import.meta.env.VITE_CHRIS_UI_URL, {
26-
token,
27-
});
21+
const user = cookie.get("username");
22+
const token: string = cookie.get(`${user}_token`);
23+
24+
// Return existing client if it exists and was created with the same token
25+
if (
26+
ChrisAPIClient.client &&
27+
((token && token === ChrisAPIClient.lastCreatedWith) ||
28+
(!token && !ChrisAPIClient.lastCreatedWith))
29+
) {
30+
return ChrisAPIClient.client;
2831
}
32+
33+
// Create new client with current token
34+
ChrisAPIClient.client = new Client(import.meta.env.VITE_CHRIS_UI_URL, {
35+
token,
36+
});
37+
38+
// Remember what token was used to create this client
39+
ChrisAPIClient.lastCreatedWith = token;
40+
2941
return ChrisAPIClient.client;
3042
}
3143

32-
static setIsTokenAuthorized(value: boolean) {
33-
ChrisAPIClient.isTokenAuthorized = value;
44+
/**
45+
* Reset the client instance (useful for testing or logout)
46+
*/
47+
static resetClient(): void {
48+
ChrisAPIClient.client = null;
49+
ChrisAPIClient.lastCreatedWith = null;
3450
}
3551
}
3652

src/components/GnomeLibrary/GnomeList.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ interface TableProps {
5656
};
5757
};
5858
computedPath: string;
59-
handleFolderClick: (folderName: string) => void;
59+
handleFolderClick: (folder: FileBrowserFolder) => void;
6060
fetchMore?: boolean;
6161
handlePagination?: () => void;
6262
filesLoading?: boolean;
@@ -518,9 +518,7 @@ const GnomeLibraryTable: React.FC<TableProps> = ({
518518
owner={r.data.owner_username}
519519
size={0}
520520
computedPath={computedPath}
521-
handleFolderClick={() =>
522-
handleFolderClick(getFolderName(r, computedPath))
523-
}
521+
handleFolderClick={() => handleFolderClick(r)}
524522
handleFileClick={() => {}}
525523
origin={origin}
526524
/>

src/components/GnomeLibrary/index.tsx

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { keepPreviousData, useQuery } from "@tanstack/react-query";
21
import { useCallback, useEffect, useState, useMemo } from "react";
32
import { useLocation, useNavigate } from "react-router-dom";
43
import { useAppSelector } from "../../store/hooks";
@@ -9,17 +8,30 @@ import GnomeCentralBreadcrumb from "./GnomeCentralBreadcrumb";
98
import GnomeLibraryTable from "./GnomeList";
109
import GnomeLibrarySidebar from "./GnomeSidebar";
1110
import styles from "./gnome.module.css";
12-
import { fetchFolders, type FolderHookData } from "./utils/hooks/useFolders";
11+
import useFolders from "./utils/hooks/useFolders";
12+
import type { FileBrowserFolder } from "@fnndsc/chrisapi";
13+
import { useQueryClient } from "@tanstack/react-query";
1314

1415
const GnomeLibrary = () => {
1516
const [activeSidebarItem, setActiveSidebarItem] = useState<string>("home");
16-
const [isFirstLoad, setIsFirstLoad] = useState(true);
1717
const [pageNumber, setPageNumber] = useState(1);
18-
const [isPaginating, setIsPaginating] = useState(false);
18+
const [isFirstLoad, setIsFirstLoad] = useState(true);
1919

2020
const { pathname } = useLocation();
2121
const navigate = useNavigate();
22+
const location = useLocation();
2223
const username = useAppSelector((state) => state.user.username);
24+
const queryClient = useQueryClient();
25+
26+
// Get selectedFolder ID from navigation state if passed
27+
const selectedFolderId = location.state?.selectedFolderId as
28+
| number
29+
| undefined;
30+
31+
// Retrieve the folder object from React Query cache if we have an ID
32+
const selectedFolder = selectedFolderId
33+
? queryClient.getQueryData<FileBrowserFolder>(["folder", selectedFolderId])
34+
: undefined;
2335

2436
const decodedPath = decodeURIComponent(pathname);
2537
const currentPathSplit = decodedPath.split("/library/")[1];
@@ -29,23 +41,15 @@ const GnomeLibrary = () => {
2941
useEffect(() => {
3042
if (computedPath) {
3143
setPageNumber(1);
32-
setIsPaginating(false);
3344
}
3445
}, [computedPath]);
3546

36-
// fetch folders, files, link files
37-
const queryKey = ["library_folders", computedPath, pageNumber];
38-
const { data, isFetching } = useQuery<FolderHookData>({
39-
queryKey: queryKey,
40-
queryFn: (): Promise<FolderHookData> => {
41-
return fetchFolders(computedPath, pageNumber, data);
42-
},
43-
placeholderData: isPaginating ? keepPreviousData : undefined,
44-
structuralSharing: true,
45-
refetchOnWindowFocus: false,
46-
refetchOnMount: false,
47-
retry: 1, // Only retry once on failure
48-
});
47+
// fetch folders, files, link files using the optimized hook
48+
const { data, isFetching } = useFolders(
49+
computedPath,
50+
pageNumber,
51+
selectedFolder,
52+
);
4953

5054
// Determine if there's more data to fetch
5155
const fetchMore =
@@ -58,7 +62,6 @@ const GnomeLibrary = () => {
5862
* Keeps placeholder data visible while fetching more items
5963
*/
6064
const handlePagination = useCallback(() => {
61-
setIsPaginating(true);
6265
setPageNumber((prevState) => prevState + 1);
6366
}, []);
6467

@@ -67,20 +70,26 @@ const GnomeLibrary = () => {
6770
* Clears placeholder data to show loading state immediately
6871
*/
6972
const navigateToPath = useCallback(
70-
(path: string) => {
71-
setIsPaginating(false);
72-
navigate(`/library/${path}`);
73+
(path: string, folder?: FileBrowserFolder) => {
74+
navigate(`/library/${path}`, {
75+
state: folder ? { selectedFolderId: folder.data.id } : {},
76+
});
7377
},
7478
[navigate],
7579
);
7680

7781
// Navigate to a folder when clicked
7882
const handleFolderClick = useCallback(
79-
(folderName: string) => {
83+
(folder: FileBrowserFolder) => {
84+
// Cache the folder object in React Query cache
85+
queryClient.setQueryData(["folder", folder.data.id], folder);
86+
87+
// Extract folder name from path
88+
const folderName = folder.data.path.split("/").pop() || "";
8089
const newPath = `${computedPath}/${folderName}`;
81-
navigateToPath(newPath);
90+
navigateToPath(newPath, folder);
8291
},
83-
[computedPath, navigateToPath],
92+
[computedPath, navigateToPath, queryClient],
8493
);
8594

8695
useEffect(() => {
@@ -173,7 +182,7 @@ const GnomeLibrary = () => {
173182
handleFolderClick={handleFolderClick}
174183
fetchMore={fetchMore}
175184
handlePagination={handlePagination}
176-
filesLoading={isFetching && isPaginating}
185+
filesLoading={isFetching}
177186
/>
178187
)
179188
)}

0 commit comments

Comments
 (0)