feat(search): lower debounce to 200ms and add AbortController using TanStack Pacer

This commit is contained in:
Fredrik Burmester
2026-01-10 23:51:16 +01:00
parent 8a7f5fd4c5
commit 781c309332
4 changed files with 101 additions and 34 deletions

View File

@@ -3,6 +3,7 @@ import type {
BaseItemKind,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { useAsyncDebouncer } from "@tanstack/react-pacer";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { Image } from "expo-image";
@@ -20,7 +21,6 @@ import {
import { useTranslation } from "react-i18next";
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useDebounce } from "use-debounce";
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
import { Input } from "@/components/common/Input";
import { Text } from "@/components/common/Text";
@@ -70,7 +70,23 @@ export default function search() {
const [searchType, setSearchType] = useState<SearchType>("Library");
const [search, setSearch] = useState<string>("");
const [debouncedSearch] = useDebounce(search, 500);
const [debouncedSearch, setDebouncedSearch] = useState("");
const abortControllerRef = useRef<AbortController | null>(null);
const searchDebouncer = useAsyncDebouncer(
async (query: string) => {
// Cancel previous in-flight requests
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
setDebouncedSearch(query);
return query;
},
{ wait: 200 },
);
useEffect(() => {
searchDebouncer.maybeExecute(search);
}, [search]);
const [api] = useAtom(apiAtom);
@@ -100,9 +116,11 @@ export default function search() {
async ({
types,
query,
signal,
}: {
types: BaseItemKind[];
query: string;
signal?: AbortSignal;
}): Promise<BaseItemDto[]> => {
if (!api || !query) {
return [];
@@ -110,13 +128,16 @@ export default function search() {
try {
if (searchEngine === "Jellyfin") {
const searchApi = await getItemsApi(api).getItems({
searchTerm: query,
limit: 10,
includeItemTypes: types,
recursive: true,
userId: user?.Id,
});
const searchApi = await getItemsApi(api).getItems(
{
searchTerm: query,
limit: 10,
includeItemTypes: types,
recursive: true,
userId: user?.Id,
},
{ signal },
);
return (searchApi.data.Items as BaseItemDto[]) || [];
}
@@ -145,6 +166,7 @@ export default function search() {
query,
searchType as "movies" | "series" | "episodes" | "actors" | "media",
10,
signal,
);
const allIds: string[] = [
@@ -159,10 +181,13 @@ export default function search() {
return [];
}
const itemsResponse = await getItemsApi(api).getItems({
ids: allIds,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
});
const itemsResponse = await getItemsApi(api).getItems(
{
ids: allIds,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
},
{ signal },
);
return (itemsResponse.data.Items as BaseItemDto[]) || [];
}
@@ -178,7 +203,7 @@ export default function search() {
.map((type) => encodeURIComponent(type))
.join("&includeItemTypes=")}`;
const response1 = await axios.get(url);
const response1 = await axios.get(url, { signal });
const ids = response1.data.ids;
@@ -186,13 +211,20 @@ export default function search() {
return [];
}
const response2 = await getItemsApi(api).getItems({
ids,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
});
const response2 = await getItemsApi(api).getItems(
{
ids,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
},
{ signal },
);
return (response2.data.Items as BaseItemDto[]) || [];
} catch (_error) {
} catch (error) {
// Silently handle aborted requests
if (error instanceof Error && error.name === "AbortError") {
return [];
}
return [];
}
},
@@ -204,25 +236,34 @@ export default function search() {
async ({
types,
query,
signal,
}: {
types: BaseItemKind[];
query: string;
signal?: AbortSignal;
}): Promise<BaseItemDto[]> => {
if (!api || !query) {
return [];
}
try {
const searchApi = await getItemsApi(api).getItems({
searchTerm: query,
limit: 10,
includeItemTypes: types,
recursive: true,
userId: user?.Id,
});
const searchApi = await getItemsApi(api).getItems(
{
searchTerm: query,
limit: 10,
includeItemTypes: types,
recursive: true,
userId: user?.Id,
},
{ signal },
);
return (searchApi.data.Items as BaseItemDto[]) || [];
} catch (_error) {
} catch (error) {
// Silently handle aborted requests
if (error instanceof Error && error.name === "AbortError") {
return [];
}
return [];
}
},
@@ -275,6 +316,7 @@ export default function search() {
searchFn({
query: debouncedSearch,
types: ["Movie"],
signal: abortControllerRef.current?.signal,
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
@@ -285,6 +327,7 @@ export default function search() {
searchFn({
query: debouncedSearch,
types: ["Series"],
signal: abortControllerRef.current?.signal,
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
@@ -295,6 +338,7 @@ export default function search() {
searchFn({
query: debouncedSearch,
types: ["Episode"],
signal: abortControllerRef.current?.signal,
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
@@ -305,6 +349,7 @@ export default function search() {
searchFn({
query: debouncedSearch,
types: ["BoxSet"],
signal: abortControllerRef.current?.signal,
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
@@ -315,6 +360,7 @@ export default function search() {
searchFn({
query: debouncedSearch,
types: ["Person"],
signal: abortControllerRef.current?.signal,
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
@@ -326,6 +372,7 @@ export default function search() {
jellyfinSearchFn({
query: debouncedSearch,
types: ["MusicArtist"],
signal: abortControllerRef.current?.signal,
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
@@ -336,6 +383,7 @@ export default function search() {
jellyfinSearchFn({
query: debouncedSearch,
types: ["MusicAlbum"],
signal: abortControllerRef.current?.signal,
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
@@ -346,6 +394,7 @@ export default function search() {
jellyfinSearchFn({
query: debouncedSearch,
types: ["Audio"],
signal: abortControllerRef.current?.signal,
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
@@ -356,6 +405,7 @@ export default function search() {
jellyfinSearchFn({
query: debouncedSearch,
types: ["Playlist"],
signal: abortControllerRef.current?.signal,
}),
enabled: searchType === "Library" && debouncedSearch.length > 0,
});