refactor: integrate useDocumentSearch hook into DocumentsSidebar for improved search functionality and performance, while enhancing user feedback with localized toast messages

This commit is contained in:
Anish Sarkar 2026-03-06 12:52:22 +05:30
parent 62c4a3befc
commit be0bfb3d56
8 changed files with 162 additions and 121 deletions

View file

@ -0,0 +1,127 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { type DocumentDisplay, toDisplayDoc } from "./use-documents";
const SEARCH_INITIAL_SIZE = 20;
const SEARCH_SCROLL_SIZE = 5;
/**
* Paginated document search hook.
*
* Handles title-based search with server-side filtering,
* pagination via skip/page_size, and staleness detection
* so fast typing never renders stale results.
*
* @param searchSpaceId - The search space to search within
* @param query - The debounced search query
* @param activeTypes - Document types to filter by
* @param enabled - When false the hook resets and stops fetching
*/
export function useDocumentSearch(
searchSpaceId: number,
query: string,
activeTypes: DocumentTypeEnum[],
enabled: boolean
) {
const [documents, setDocuments] = useState<DocumentDisplay[]>([]);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(false);
const [error, setError] = useState(false);
const apiLoadedRef = useRef(0);
const queryRef = useRef(query);
const isActive = enabled && !!query.trim();
const activeTypesKey = activeTypes.join(",");
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTypesKey serializes activeTypes
useEffect(() => {
if (!isActive || !searchSpaceId) {
setDocuments([]);
setHasMore(false);
setError(false);
apiLoadedRef.current = 0;
return;
}
let cancelled = false;
queryRef.current = query;
setLoading(true);
setError(false);
documentsApiService
.searchDocuments({
queryParams: {
search_space_id: searchSpaceId,
page: 0,
page_size: SEARCH_INITIAL_SIZE,
title: query.trim(),
...(activeTypes.length > 0 && { document_types: activeTypes }),
},
})
.then((response) => {
if (cancelled || queryRef.current !== query) return;
setDocuments(response.items.map(toDisplayDoc));
setHasMore(response.has_more);
apiLoadedRef.current = response.items.length;
})
.catch((err) => {
if (cancelled) return;
console.error("[useDocumentSearch] Search failed:", err);
setError(true);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [query, searchSpaceId, isActive, activeTypesKey]);
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTypesKey serializes activeTypes
const loadMore = useCallback(async () => {
if (loadingMore || !isActive || !hasMore) return;
setLoadingMore(true);
try {
const response = await documentsApiService.searchDocuments({
queryParams: {
search_space_id: searchSpaceId,
skip: apiLoadedRef.current,
page_size: SEARCH_SCROLL_SIZE,
title: query.trim(),
...(activeTypes.length > 0 && { document_types: activeTypes }),
},
});
if (queryRef.current !== query) return;
setDocuments((prev) => [...prev, ...response.items.map(toDisplayDoc)]);
setHasMore(response.has_more);
apiLoadedRef.current += response.items.length;
} catch (err) {
console.error("[useDocumentSearch] Load more failed:", err);
} finally {
setLoadingMore(false);
}
}, [loadingMore, isActive, hasMore, searchSpaceId, query, activeTypesKey]);
const removeItems = useCallback((ids: number[]) => {
const idSet = new Set(ids);
setDocuments((prev) => prev.filter((item) => !idSet.has(item.id)));
}, []);
return {
documents,
loading,
loadingMore,
hasMore,
loadMore,
error,
removeItems,
};
}

View file

@ -105,7 +105,6 @@ export function useDocuments(
// Snapshot of all doc IDs from Electric's first callback after initial load.
// Anything appearing in subsequent callbacks NOT in this set is genuinely new.
const electricBaselineIdsRef = useRef<Set<number> | null>(null);
const knownApiIdsRef = useRef<Set<number>>(new Set());
const userCacheRef = useRef<Map<string, string>>(new Map());
const emailCacheRef = useRef<Map<string, string>>(new Map());
const syncHandleRef = useRef<SyncHandle | null>(null);
@ -178,7 +177,6 @@ export function useDocuments(
apiLoadedCountRef.current = 0;
initialLoadDoneRef.current = false;
electricBaselineIdsRef.current = null;
knownApiIdsRef.current = new Set();
const fetchInitialData = async () => {
try {
@ -209,9 +207,6 @@ export function useDocuments(
setError(null);
apiLoadedCountRef.current = docsResponse.items.length;
initialLoadDoneRef.current = true;
for (const doc of docs) {
knownApiIdsRef.current.add(doc.id);
}
} catch (err) {
if (cancelled) return;
console.error("[useDocuments] Initial load failed:", err);
@ -454,7 +449,6 @@ export function useDocuments(
apiLoadedCountRef.current = 0;
initialLoadDoneRef.current = false;
electricBaselineIdsRef.current = null;
knownApiIdsRef.current = new Set();
userCacheRef.current.clear();
emailCacheRef.current.clear();
}
@ -481,9 +475,6 @@ export function useDocuments(
populateUserCache(response.items);
const newDocs = response.items.map(apiToDisplayDoc);
for (const doc of newDocs) {
knownApiIdsRef.current.add(doc.id);
}
setDocuments((prev) => {
const existingIds = new Set(prev.map((d) => d.id));