diff --git a/src/api/queries/search/index.ts b/src/api/queries/search/index.ts new file mode 100644 index 00000000..db0866d8 --- /dev/null +++ b/src/api/queries/search/index.ts @@ -0,0 +1,19 @@ +import { QueryKeys } from '../../../enums/query-keys' +import { useQuery } from '@tanstack/react-query' +import { fetchSearchResults } from './utils' +import { ONE_MINUTE } from '../../../constants/query-client' +import { useJellifyLibrary } from '../../../stores' + +const useSearchResults = (searchString: string | undefined) => { + const [library] = useJellifyLibrary() + + return useQuery({ + queryKey: [QueryKeys.Search, library?.musicLibraryId, searchString], + queryFn: () => fetchSearchResults(library?.musicLibraryId, searchString), + staleTime: ONE_MINUTE * 10, // Cache results for 10 minutes + gcTime: ONE_MINUTE * 15, // Garbage collect after 15 minutes + enabled: !!library?.musicLibraryId && !!searchString, // Only run if we have a library ID and a search string + }) +} + +export default useSearchResults diff --git a/src/api/queries/search.ts b/src/api/queries/search/utils/index.ts similarity index 87% rename from src/api/queries/search.ts rename to src/api/queries/search/utils/index.ts index 693d4984..b8b43280 100644 --- a/src/api/queries/search.ts +++ b/src/api/queries/search/utils/index.ts @@ -1,9 +1,8 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { getItemsApi } from '@jellyfin/sdk/lib/utils/api' import { isEmpty, isUndefined, trim } from 'lodash' -import QueryConfig from '../../configs/query.config' -import { Api } from '@jellyfin/sdk' -import { JellifyUser } from '../../types/JellifyUser' +import QueryConfig from '../../../../configs/query.config' +import { getApi, getUser } from '../../../../stores' /** * Performs a search for items against the Jellyfin server, trimming whitespace * around the search term for the best possible results. @@ -11,12 +10,13 @@ import { JellifyUser } from '../../types/JellifyUser' * @returns A promise of a BaseItemDto array, be it empty or not */ export async function fetchSearchResults( - api: Api | undefined, - user: JellifyUser | undefined, libraryId: string | undefined, searchString: string | undefined, ): Promise { return new Promise((resolve, reject) => { + const api = getApi() + const user = getUser() + if (isEmpty(searchString)) resolve([]) if (isUndefined(api)) return reject('Client instance not set') diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 2f7c3f21..371c8a5e 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,11 +1,8 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import Input from '../Global/helpers/input' import { H5, Text } from '../Global/helpers/text' import ItemRow from '../Global/components/item-row' import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { QueryKeys } from '../../enums/query-keys' -import { fetchSearchResults } from '../../api/queries/search' -import { useQuery } from '@tanstack/react-query' import { getToken, H3, Spinner, YStack } from 'tamagui' import Suggestions from './suggestions' import { isEmpty } from 'lodash' @@ -13,7 +10,6 @@ import HorizontalCardList from '../Global/components/horizontal-list' import ItemCard from '../Global/components/item-card' import SearchParamList from '../../screens/Search/types' import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry' -import { getApi, getUser, useJellifyLibrary } from '../../stores' import { FlashList } from '@shopify/flash-list' import navigationRef from '../../../navigation' import { StackActions } from '@react-navigation/native' @@ -22,41 +18,35 @@ import Track from '../Global/components/Track' import { pickRandomItemFromArray } from '../../utils/parsing/random' import { SEARCH_PLACEHOLDERS } from '../../configs/placeholder.config' import { formatArtistName } from '../../utils/formatting/artist-names' +import useSearchResults from '../../api/queries/search' export default function Search({ navigation, }: { navigation: NativeStackNavigationProp }): React.JSX.Element { - const api = getApi() - const user = getUser() - const [library] = useJellifyLibrary() + /** + * Raw text input value from the user, updates immediately as they type + */ + const [inputValue, setInputValue] = useState(undefined) + /** + * Debounced search string that updates 500ms after the user stops typing, used to trigger the search query + * which is keyed off of this value for caching. + */ const [searchString, setSearchString] = useState(undefined) - const { - data: items, - refetch, - isFetching: fetchingResults, - } = useQuery({ - queryKey: [QueryKeys.Search, library?.musicLibraryId, searchString], - queryFn: () => fetchSearchResults(api, user, library?.musicLibraryId, searchString), - }) + useEffect(() => { + const timeout = setTimeout(() => { + setSearchString(inputValue || undefined) + }, 500) + return () => clearTimeout(timeout) + }, [inputValue]) - const search = () => { - let timeout: ReturnType - - return () => { - clearTimeout(timeout) - timeout = setTimeout(() => { - refetch() - }, 1000) - } - } + const { data: items, isFetching: fetchingResults } = useSearchResults(searchString) const handleSearchStringUpdate = (value: string | undefined) => { - setSearchString(value) - search() + setInputValue(value || undefined) } const handleScrollBeginDrag = () => { @@ -90,7 +80,7 @@ export default function Search({