Commit 0aef8999 authored by isstuev's avatar isstuev

quick search

parent 93099d26
...@@ -40,7 +40,7 @@ import type { L2TxnBatchesResponse } from 'types/api/l2TxnBatches'; ...@@ -40,7 +40,7 @@ import type { L2TxnBatchesResponse } from 'types/api/l2TxnBatches';
import type { L2WithdrawalsResponse } from 'types/api/l2Withdrawals'; import type { L2WithdrawalsResponse } from 'types/api/l2Withdrawals';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchRedirectResult, SearchResult, SearchResultFilters } from 'types/api/search'; import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { import type {
TokenCounters, TokenCounters,
...@@ -420,6 +420,10 @@ export const RESOURCES = { ...@@ -420,6 +420,10 @@ export const RESOURCES = {
}, },
// SEARCH // SEARCH
quick_search: {
path: '/api/v2/search/quick',
filterFields: [ 'q' ],
},
search: { search: {
path: '/api/v2/search', path: '/api/v2/search',
filterFields: [ 'q' ], filterFields: [ 'q' ],
...@@ -597,6 +601,7 @@ Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse : ...@@ -597,6 +601,7 @@ Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_instance_holders' ? TokenHolders : Q extends 'token_instance_holders' ? TokenHolders :
Q extends 'token_inventory' ? TokenInventoryResponse : Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse : Q extends 'tokens' ? TokensResponse :
Q extends 'quick_search' ? Array<SearchResultItem> :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'search_check_redirect' ? SearchRedirectResult : Q extends 'search_check_redirect' ? SearchRedirectResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
......
...@@ -21,7 +21,7 @@ import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery'; ...@@ -21,7 +21,7 @@ import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
const SearchResultsPageContent = () => { const SearchResultsPageContent = () => {
const router = useRouter(); const router = useRouter();
const { query, redirectCheckQuery, searchTerm, debouncedSearchTerm, handleSearchTermChange } = useSearchQuery(true); const { query, redirectCheckQuery, searchTerm, debouncedSearchTerm, handleSearchTermChange } = useSearchQuery();
const { data, isError, isPlaceholderData, pagination } = query; const { data, isError, isPlaceholderData, pagination } = query;
const [ showContent, setShowContent ] = React.useState(false); const [ showContent, setShowContent ] = React.useState(false);
......
...@@ -30,15 +30,13 @@ test.beforeEach(async({ page }) => { ...@@ -30,15 +30,13 @@ test.beforeEach(async({ page }) => {
}); });
test('search by token name +@mobile +@dark-mode', async({ mount, page }) => { test('search by token name +@mobile +@dark-mode', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + '?q=o'; const API_URL = buildApiUrl('quick_search') + '?q=o';
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.token1, searchMock.token1,
searchMock.token2, searchMock.token2,
], ]),
}),
})); }));
await mount( await mount(
...@@ -53,14 +51,12 @@ test('search by token name +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -53,14 +51,12 @@ test('search by token name +@mobile +@dark-mode', async({ mount, page }) => {
}); });
test('search by contract name +@mobile +@dark-mode', async({ mount, page }) => { test('search by contract name +@mobile +@dark-mode', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + '?q=o'; const API_URL = buildApiUrl('quick_search') + '?q=o';
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.contract1, searchMock.contract1,
], ]),
}),
})); }));
await mount( await mount(
...@@ -75,16 +71,14 @@ test('search by contract name +@mobile +@dark-mode', async({ mount, page }) => ...@@ -75,16 +71,14 @@ test('search by contract name +@mobile +@dark-mode', async({ mount, page }) =>
}); });
test('search by name homepage +@dark-mode', async({ mount, page }) => { test('search by name homepage +@dark-mode', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + '?q=o'; const API_URL = buildApiUrl('quick_search') + '?q=o';
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.token1, searchMock.token1,
searchMock.token2, searchMock.token2,
searchMock.contract1, searchMock.contract1,
], ]),
}),
})); }));
await mount( await mount(
...@@ -101,14 +95,12 @@ test('search by name homepage +@dark-mode', async({ mount, page }) => { ...@@ -101,14 +95,12 @@ test('search by name homepage +@dark-mode', async({ mount, page }) => {
}); });
test('search by tag +@mobile +@dark-mode', async({ mount, page }) => { test('search by tag +@mobile +@dark-mode', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + '?q=o'; const API_URL = buildApiUrl('quick_search') + '?q=o';
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.label1, searchMock.label1,
], ]),
}),
})); }));
await mount( await mount(
...@@ -123,14 +115,12 @@ test('search by tag +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -123,14 +115,12 @@ test('search by tag +@mobile +@dark-mode', async({ mount, page }) => {
}); });
test('search by address hash +@mobile', async({ mount, page }) => { test('search by address hash +@mobile', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + `?q=${ searchMock.address1.address }`; const API_URL = buildApiUrl('quick_search') + `?q=${ searchMock.address1.address }`;
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.address1, searchMock.address1,
], ]),
}),
})); }));
await mount( await mount(
...@@ -145,14 +135,12 @@ test('search by address hash +@mobile', async({ mount, page }) => { ...@@ -145,14 +135,12 @@ test('search by address hash +@mobile', async({ mount, page }) => {
}); });
test('search by block number +@mobile', async({ mount, page }) => { test('search by block number +@mobile', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + `?q=${ searchMock.block1.block_number }`; const API_URL = buildApiUrl('quick_search') + `?q=${ searchMock.block1.block_number }`;
await page.route(buildApiUrl('search') + `?q=${ searchMock.block1.block_number }`, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.block1, searchMock.block1,
], ]),
}),
})); }));
await mount( await mount(
...@@ -167,14 +155,12 @@ test('search by block number +@mobile', async({ mount, page }) => { ...@@ -167,14 +155,12 @@ test('search by block number +@mobile', async({ mount, page }) => {
}); });
test('search by block hash +@mobile', async({ mount, page }) => { test('search by block hash +@mobile', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + `?q=${ searchMock.block1.block_hash }`; const API_URL = buildApiUrl('quick_search') + `?q=${ searchMock.block1.block_hash }`;
await page.route(buildApiUrl('search') + `?q=${ searchMock.block1.block_hash }`, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.block1, searchMock.block1,
], ]),
}),
})); }));
await mount( await mount(
...@@ -189,14 +175,12 @@ test('search by block hash +@mobile', async({ mount, page }) => { ...@@ -189,14 +175,12 @@ test('search by block hash +@mobile', async({ mount, page }) => {
}); });
test('search by tx hash +@mobile', async({ mount, page }) => { test('search by tx hash +@mobile', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + `?q=${ searchMock.tx1.tx_hash }`; const API_URL = buildApiUrl('quick_search') + `?q=${ searchMock.tx1.tx_hash }`;
await page.route(buildApiUrl('search') + `?q=${ searchMock.tx1.tx_hash }`, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.tx1, searchMock.tx1,
], ]),
}),
})); }));
await mount( await mount(
...@@ -211,17 +195,15 @@ test('search by tx hash +@mobile', async({ mount, page }) => { ...@@ -211,17 +195,15 @@ test('search by tx hash +@mobile', async({ mount, page }) => {
}); });
test('search with view all link', async({ mount, page }) => { test('search with view all link', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + '?q=o'; const API_URL = buildApiUrl('quick_search') + '?q=o';
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.token1, searchMock.token1,
searchMock.token2, searchMock.token2,
searchMock.contract1, searchMock.contract1,
], ...Array(47).fill(searchMock.contract1),
next_page_params: { foo: 'bar' }, ]),
}),
})); }));
await mount( await mount(
...@@ -237,11 +219,10 @@ test('search with view all link', async({ mount, page }) => { ...@@ -237,11 +219,10 @@ test('search with view all link', async({ mount, page }) => {
}); });
test('scroll suggest to category', async({ mount, page }) => { test('scroll suggest to category', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + '?q=o'; const API_URL = buildApiUrl('quick_search') + '?q=o';
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.token1, searchMock.token1,
searchMock.token2, searchMock.token2,
searchMock.contract1, searchMock.contract1,
...@@ -254,8 +235,7 @@ test('scroll suggest to category', async({ mount, page }) => { ...@@ -254,8 +235,7 @@ test('scroll suggest to category', async({ mount, page }) => {
searchMock.token1, searchMock.token1,
searchMock.token2, searchMock.token2,
searchMock.contract1, searchMock.contract1,
], ]),
}),
})); }));
await mount( await mount(
...@@ -287,15 +267,12 @@ test.describe('with apps', () => { ...@@ -287,15 +267,12 @@ test.describe('with apps', () => {
const MARKETPLACE_CONFIG_URL = 'https://localhost:3000/marketplace-config.json'; const MARKETPLACE_CONFIG_URL = 'https://localhost:3000/marketplace-config.json';
test('default view +@mobile', async({ mount, page }) => { test('default view +@mobile', async({ mount, page }) => {
const API_URL = buildApiUrl('search') + '?q=o'; const API_URL = buildApiUrl('quick_search') + '?q=o';
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ body: JSON.stringify([
items: [
searchMock.token1, searchMock.token1,
], ]),
next_page_params: { foo: 'bar' },
}),
})); }));
await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({ await page.route(MARKETPLACE_CONFIG_URL, (route) => route.fulfill({
......
...@@ -15,7 +15,7 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -15,7 +15,7 @@ import LinkInternal from 'ui/shared/LinkInternal';
import SearchBarInput from './SearchBarInput'; import SearchBarInput from './SearchBarInput';
import SearchBarRecentKeywords from './SearchBarRecentKeywords'; import SearchBarRecentKeywords from './SearchBarRecentKeywords';
import SearchBarSuggest from './SearchBarSuggest/SearchBarSuggest'; import SearchBarSuggest from './SearchBarSuggest/SearchBarSuggest';
import useSearchQuery from './useSearchQuery'; import useQuickSearchQuery from './useQuickSearchQuery';
type Props = { type Props = {
isHomepage?: boolean; isHomepage?: boolean;
...@@ -34,7 +34,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -34,7 +34,7 @@ const SearchBar = ({ isHomepage }: Props) => {
const recentSearchKeywords = getRecentSearchKeywords(); const recentSearchKeywords = getRecentSearchKeywords();
const { searchTerm, debouncedSearchTerm, handleSearchTermChange, query, pathname } = useSearchQuery(); const { searchTerm, debouncedSearchTerm, handleSearchTermChange, query, pathname } = useQuickSearchQuery();
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
...@@ -160,7 +160,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -160,7 +160,7 @@ const SearchBar = ({ isHomepage }: Props) => {
) } ) }
</Box> </Box>
</PopoverBody> </PopoverBody>
{ searchTerm.trim().length > 0 && query.data?.next_page_params && ( { searchTerm.trim().length > 0 && query.data && query.data.length >= 50 && (
<PopoverFooter> <PopoverFooter>
<LinkInternal <LinkInternal
href={ route({ pathname: '/search-results', query: { q: searchTerm } }) } href={ route({ pathname: '/search-results', query: { q: searchTerm } }) }
......
import { Box, Tab, TabList, Tabs, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Tab, TabList, Tabs, Text, useColorModeValue } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import type { SearchResultItem } from 'types/api/search';
import type { ResourceError } from 'lib/api/resources';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useMarketplaceApps from 'ui/marketplace/useMarketplaceApps'; import useMarketplaceApps from 'ui/marketplace/useMarketplaceApps';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import type { ApiCategory, ItemsCategoriesMap } from 'ui/shared/search/utils'; import type { ApiCategory, ItemsCategoriesMap } from 'ui/shared/search/utils';
import { getItemCategory, searchCategories } from 'ui/shared/search/utils'; import { getItemCategory, searchCategories } from 'ui/shared/search/utils';
...@@ -15,7 +18,7 @@ import SearchBarSuggestApp from './SearchBarSuggestApp'; ...@@ -15,7 +18,7 @@ import SearchBarSuggestApp from './SearchBarSuggestApp';
import SearchBarSuggestItem from './SearchBarSuggestItem'; import SearchBarSuggestItem from './SearchBarSuggestItem';
interface Props { interface Props {
query: QueryWithPagesResult<'search'>; query: UseQueryResult<Array<SearchResultItem>, ResourceError<unknown>>;
searchTerm: string; searchTerm: string;
onItemClick: (event: React.MouseEvent<HTMLAnchorElement>) => void; onItemClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
containerId: string; containerId: string;
...@@ -33,7 +36,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -33,7 +36,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
const handleScroll = React.useCallback(() => { const handleScroll = React.useCallback(() => {
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
if (!container || !query.data?.items.length) { if (!container || !query.data?.length) {
return; return;
} }
const topLimit = container.getBoundingClientRect().y + (tabsRef.current?.clientHeight || 0) + 24; const topLimit = container.getBoundingClientRect().y + (tabsRef.current?.clientHeight || 0) + 24;
...@@ -47,7 +50,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -47,7 +50,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
break; break;
} }
} }
}, [ containerId, query.data?.items ]); }, [ containerId, query.data ]);
React.useEffect(() => { React.useEffect(() => {
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
...@@ -63,11 +66,11 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -63,11 +66,11 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
}, [ containerId, handleScroll ]); }, [ containerId, handleScroll ]);
const itemsGroups = React.useMemo(() => { const itemsGroups = React.useMemo(() => {
if (!query.data?.items && !marketplaceApps.displayedApps) { if (!query.data && !marketplaceApps.displayedApps) {
return {}; return {};
} }
const map: Partial<ItemsCategoriesMap> = {}; const map: Partial<ItemsCategoriesMap> = {};
query.data?.items.forEach(item => { query.data?.forEach(item => {
const cat = getItemCategory(item) as ApiCategory; const cat = getItemCategory(item) as ApiCategory;
if (cat) { if (cat) {
if (cat in map) { if (cat in map) {
...@@ -81,7 +84,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -81,7 +84,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
map.app = marketplaceApps.displayedApps; map.app = marketplaceApps.displayedApps;
} }
return map; return map;
}, [ query.data?.items, marketplaceApps.displayedApps ]); }, [ query.data, marketplaceApps.displayedApps ]);
const scrollToCategory = React.useCallback((index: number) => () => { const scrollToCategory = React.useCallback((index: number) => () => {
setTabIndex(index); setTabIndex(index);
...@@ -104,7 +107,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -104,7 +107,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
return <Text>Something went wrong. Try refreshing the page or come back later.</Text>; return <Text>Something went wrong. Try refreshing the page or come back later.</Text>;
} }
if (!query.data.items || query.data.items.length === 0) { if (!query.data || query.data.length === 0) {
return <Text>No results found.</Text>; return <Text>No results found.</Text>;
} }
......
import { useRouter } from 'next/router';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
export default function useSearchQuery() {
const router = useRouter();
const [ searchTerm, setSearchTerm ] = React.useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const pathname = router.pathname;
const query = useApiQuery('quick_search', {
queryParams: { q: debouncedSearchTerm },
queryOptions: { enabled: debouncedSearchTerm.trim().length > 0 },
});
const redirectCheckQuery = useApiQuery('search_check_redirect', {
// on pages with regular search bar we check redirect on every search term change
// in order to prepend its result to suggest list since this resource is much faster than regular search
queryParams: { q: debouncedSearchTerm },
queryOptions: { enabled: Boolean(debouncedSearchTerm) },
});
return React.useMemo(() => ({
searchTerm,
debouncedSearchTerm,
handleSearchTermChange: setSearchTerm,
query,
redirectCheckQuery,
pathname,
}), [ debouncedSearchTerm, pathname, query, redirectCheckQuery, searchTerm ]);
}
...@@ -9,10 +9,10 @@ import { SEARCH_RESULT_ITEM, SEARCH_RESULT_NEXT_PAGE_PARAMS } from 'stubs/search ...@@ -9,10 +9,10 @@ import { SEARCH_RESULT_ITEM, SEARCH_RESULT_NEXT_PAGE_PARAMS } from 'stubs/search
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
export default function useSearchQuery(isSearchPage = false) { export default function useSearchQuery() {
const router = useRouter(); const router = useRouter();
const q = React.useRef(getQueryParamString(router.query.q)); const q = React.useRef(getQueryParamString(router.query.q));
const initialValue = isSearchPage ? q.current : ''; const initialValue = q.current;
const [ searchTerm, setSearchTerm ] = React.useState(initialValue); const [ searchTerm, setSearchTerm ] = React.useState(initialValue);
...@@ -24,24 +24,18 @@ export default function useSearchQuery(isSearchPage = false) { ...@@ -24,24 +24,18 @@ export default function useSearchQuery(isSearchPage = false) {
filters: { q: debouncedSearchTerm }, filters: { q: debouncedSearchTerm },
options: { options: {
enabled: debouncedSearchTerm.trim().length > 0, enabled: debouncedSearchTerm.trim().length > 0,
placeholderData: isSearchPage ? placeholderData: generateListStub<'search'>(SEARCH_RESULT_ITEM, 50, { next_page_params: SEARCH_RESULT_NEXT_PAGE_PARAMS }),
generateListStub<'search'>(SEARCH_RESULT_ITEM, 50, { next_page_params: SEARCH_RESULT_NEXT_PAGE_PARAMS }) :
undefined,
}, },
}); });
const redirectCheckQuery = useApiQuery('search_check_redirect', { const redirectCheckQuery = useApiQuery('search_check_redirect', {
// on search result page we check redirect only once on mount // on search result page we check redirect only once on mount
// on pages with regular search bar we check redirect on every search term change queryParams: { q: q.current },
// in order to prepend its result to suggest list since this resource is much faster than regular search queryOptions: { enabled: Boolean(q.current) },
queryParams: { q: isSearchPage ? q.current : debouncedSearchTerm },
queryOptions: { enabled: Boolean(isSearchPage ? q.current : debouncedSearchTerm) },
}); });
useUpdateValueEffect(() => { useUpdateValueEffect(() => {
if (isSearchPage) {
query.onFilterChange({ q: debouncedSearchTerm }); query.onFilterChange({ q: debouncedSearchTerm });
}
}, debouncedSearchTerm); }, debouncedSearchTerm);
return React.useMemo(() => ({ return React.useMemo(() => ({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment