refactor: search query hook (#1014)

* refactor: search query hook

update staleTime and gcTime for search query

move query into queries folder alongside function call

update usage in search component

* debounce searches properly
This commit is contained in:
Violet Caulfield
2026-02-23 18:15:24 -06:00
committed by GitHub
parent c01d195876
commit 038f0d0715
3 changed files with 43 additions and 34 deletions

View File

@@ -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

View File

@@ -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<BaseItemDto[]> {
return new Promise((resolve, reject) => {
const api = getApi()
const user = getUser()
if (isEmpty(searchString)) resolve([])
if (isUndefined(api)) return reject('Client instance not set')

View File

@@ -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<SearchParamList, 'SearchScreen'>
}): 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<string | undefined>(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<string | undefined>(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<typeof setTimeout>
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({
<Input
placeholder={placeholder}
onChangeText={handleSearchStringUpdate}
value={searchString}
value={inputValue}
testID='search-input'
clearButtonMode='always'
/>