From ebb87b3d45a8ba920d9f86374966cb67ab4962f8 Mon Sep 17 00:00:00 2001 From: Alisher Musurmonov Date: Mon, 25 Nov 2024 17:54:25 +0500 Subject: [PATCH] UIF-562: add `useDebouncedQuery` hook to fix endless request for `DynamicSelection` component --- CHANGELOG.md | 1 + lib/DynamicSelection/DynamicSelection.js | 63 +++++++------------ lib/hooks/index.js | 1 + lib/hooks/useDebouncedQuery/index.js | 1 + .../useDebouncedQuery/useDebouncedQuery.js | 61 ++++++++++++++++++ 5 files changed, 86 insertions(+), 41 deletions(-) create mode 100644 lib/hooks/useDebouncedQuery/index.js create mode 100644 lib/hooks/useDebouncedQuery/useDebouncedQuery.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5680d86d..25cb15d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Add more reusable hooks and utilities. Refs UISACQCOMP-228. * Move reusable version history components to the ACQ lib. Refs UISACQCOMP-230. * Move reusable helper function to support version history functionality. Refs UISACQCOMP-232. +* Add `useDebouncedQuery` hook to fix endless request for `DynamicSelection` component. Refs UIF-562. ## [6.0.1](https://github.com/folio-org/stripes-acq-components/tree/v6.0.1) (2024-11-14) [Full Changelog](https://github.com/folio-org/stripes-acq-components/compare/v6.0.0...v6.0.1) diff --git a/lib/DynamicSelection/DynamicSelection.js b/lib/DynamicSelection/DynamicSelection.js index 7d5f7a1f..343a6fc0 100644 --- a/lib/DynamicSelection/DynamicSelection.js +++ b/lib/DynamicSelection/DynamicSelection.js @@ -1,13 +1,13 @@ -import { useCallback, useEffect, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { debounce } from 'lodash'; import PropTypes from 'prop-types'; +import { useCallback } from 'react'; +import { FormattedMessage } from 'react-intl'; -import { Loading, Selection } from '@folio/stripes/components'; -import { useOkapiKy } from '@folio/stripes/core'; +import { + Loading, + Selection, +} from '@folio/stripes/components'; -const LIST_ITEMS_LIMIT = 100; -const DEBOUNCE_DELAY = 500; +import { useDebouncedQuery } from '../hooks'; export const DynamicSelection = ({ api, @@ -19,46 +19,27 @@ export const DynamicSelection = ({ value, ...rest }) => { - const ky = useOkapiKy(); - const [filterValue, setFilterValue] = useState(''); - const [options, setOptions] = useState(initialOptions); - const [isLoading, setIsLoading] = useState(); - - const fetchData = useCallback(debounce(async (inputValue) => { - const searchParams = { - query: queryBuilder(inputValue), - limit: LIST_ITEMS_LIMIT, - }; - - try { - const res = await ky.get(api, { searchParams }).json(); - - setOptions(dataFormatter(res)); - } catch { - setOptions([]); - } - - setIsLoading(false); - }, DEBOUNCE_DELAY), []); - - const onFilter = useCallback((inputValue) => { - setIsLoading(true); - setFilterValue(inputValue); - fetchData(inputValue); + const { + options = initialOptions, + isLoading, + inputValue, + setInputValue, + } = useDebouncedQuery({ + api, + dataFormatter, + queryBuilder, + }); + + const onFilter = useCallback((filterValue) => { + setInputValue(filterValue); return options; - }, [options, fetchData]); - - useEffect(() => { - return () => { - fetchData.cancel(); - }; - }, []); + }, [options, setInputValue]); return ( } + emptyMessage={!inputValue && } loading={isLoading} loadingMessage={} name={name} diff --git a/lib/hooks/index.js b/lib/hooks/index.js index daf5dc20..19723f7f 100644 --- a/lib/hooks/index.js +++ b/lib/hooks/index.js @@ -9,6 +9,7 @@ export * from './useCampuses'; export * from './useCampusesQuery'; export * from './useCategories'; export * from './useContributorNameTypes'; +export * from './useDebouncedQuery'; export * from './useDefaultReceivingSearchSettings'; export * from './useEventEmitter'; export * from './useExchangeRateValue'; diff --git a/lib/hooks/useDebouncedQuery/index.js b/lib/hooks/useDebouncedQuery/index.js new file mode 100644 index 00000000..6543f80a --- /dev/null +++ b/lib/hooks/useDebouncedQuery/index.js @@ -0,0 +1 @@ +export { useDebouncedQuery } from './useDebouncedQuery'; diff --git a/lib/hooks/useDebouncedQuery/useDebouncedQuery.js b/lib/hooks/useDebouncedQuery/useDebouncedQuery.js new file mode 100644 index 00000000..76b14d82 --- /dev/null +++ b/lib/hooks/useDebouncedQuery/useDebouncedQuery.js @@ -0,0 +1,61 @@ +import debounce from 'lodash/debounce'; +import { + useMemo, + useState, +} from 'react'; +import { useQuery } from 'react-query'; + +import { + useNamespace, + useOkapiKy, +} from '@folio/stripes/core'; + +const LIST_ITEMS_LIMIT = 100; +const DEBOUNCE_DELAY = 500; + +export const useDebouncedQuery = ({ + api, + queryBuilder, + dataFormatter, + debounceDelay = DEBOUNCE_DELAY, + limit = LIST_ITEMS_LIMIT, +}) => { + const [inputValue, setInputValue] = useState(''); + const [options, setOptions] = useState([]); + const ky = useOkapiKy(); + const [namespace] = useNamespace({ key: 'locations' }); + + const debouncedSetInputValue = useMemo(() => { + return debounce((value) => setInputValue(value), debounceDelay); + }, [debounceDelay]); + + const { isLoading } = useQuery({ + queryKey: [namespace, inputValue], + queryFn: async ({ signal }) => { + if (!inputValue) return []; + + const searchParams = { + query: queryBuilder(inputValue), + limit, + }; + + const res = await ky.get(api, { searchParams, signal }).json(); + + return dataFormatter(res); + }, + enabled: Boolean(inputValue), + onSuccess: (data) => { + setOptions(data); + }, + onError: () => { + setOptions([]); + }, + }); + + return { + options, + isLoading, + inputValue, + setInputValue: debouncedSetInputValue, + }; +};