Feature/improve refactored global search performance (#317)
* Move "search term" retrieval to parent component Preparation for reducing re-renders. Each "SourceSearchPreview" component would re-render at each key press. * Memoize "SourceSearchPreview" components Only re-render in case the "searchString" (or other props) changed. Due to the query the components rendered everytime a key was pressed in the search field * Hide "SourceSearchPreview" immediately in case the search query is empty Do not wait for the debounced "searchString" in case it will be an empty string anyways * Abort outdated search requests In case many sources are being searched, the outdated requests can block the new ones. To prevent this, the old ones will get canceled. * Debounce source search result state changes In case multiple search requests are finished in short succession, each state update would trigger an update. By debouncing these updates, the search results do not "jump around" as much, since the sources get sorted by search request state and result
This commit is contained in:
@@ -78,16 +78,24 @@ const compareSourcesBySearchResult = (
|
|||||||
};
|
};
|
||||||
const TRIGGER_SEARCH_THRESHOLD = 1000; // ms
|
const TRIGGER_SEARCH_THRESHOLD = 1000; // ms
|
||||||
|
|
||||||
const SourceSearchPreview = ({
|
const SourceSearchPreview = React.memo(
|
||||||
|
({
|
||||||
source,
|
source,
|
||||||
onSearchRequestFinished,
|
onSearchRequestFinished,
|
||||||
|
searchString,
|
||||||
|
emptyQuery,
|
||||||
}: {
|
}: {
|
||||||
source: ISource;
|
source: ISource;
|
||||||
onSearchRequestFinished: (source: ISource, isLoading: boolean, hasResults: boolean, emptySearch: boolean) => void;
|
onSearchRequestFinished: (
|
||||||
|
source: ISource,
|
||||||
|
isLoading: boolean,
|
||||||
|
hasResults: boolean,
|
||||||
|
emptySearch: boolean,
|
||||||
|
) => void;
|
||||||
|
searchString: string | null | undefined;
|
||||||
|
emptyQuery: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [query] = useQueryParam('query', StringParam);
|
|
||||||
const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD);
|
|
||||||
const skipRequest = !searchString;
|
const skipRequest = !searchString;
|
||||||
|
|
||||||
const { id, displayName, lang } = source;
|
const { id, displayName, lang } = source;
|
||||||
@@ -97,6 +105,7 @@ const SourceSearchPreview = ({
|
|||||||
setSize,
|
setSize,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
|
abortRequest,
|
||||||
} = requestManager.useSourceSearch(id, searchString ?? '', 1, { skipRequest });
|
} = requestManager.useSourceSearch(id, searchString ?? '', 1, { skipRequest });
|
||||||
const mangas = !isLoading ? searchResult?.[0]?.mangaList ?? [] : [];
|
const mangas = !isLoading ? searchResult?.[0]?.mangaList ?? [] : [];
|
||||||
const noMangasFound = !isLoading && !mangas.length;
|
const noMangasFound = !isLoading && !mangas.length;
|
||||||
@@ -105,10 +114,6 @@ const SourceSearchPreview = ({
|
|||||||
onSearchRequestFinished(source, isLoading, !noMangasFound, !searchString);
|
onSearchRequestFinished(source, isLoading, !noMangasFound, !searchString);
|
||||||
}, [isLoading, noMangasFound, searchString]);
|
}, [isLoading, noMangasFound, searchString]);
|
||||||
|
|
||||||
if (!isLoading && !searchString) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let errorMessage: string | undefined;
|
let errorMessage: string | undefined;
|
||||||
if (error) {
|
if (error) {
|
||||||
errorMessage = t('search.error.label.source_search_failed');
|
errorMessage = t('search.error.label.source_search_failed');
|
||||||
@@ -116,10 +121,27 @@ const SourceSearchPreview = ({
|
|||||||
errorMessage = t('manga.error.label.no_mangas_found');
|
errorMessage = t('manga.error.label.no_mangas_found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
abortRequest(
|
||||||
|
new Error(`SourceSearchPreview(${source.id}, ${source.displayName}): search string changed`),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[searchString],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((!isLoading && !searchString) || emptyQuery) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card sx={{ margin: '10px' }}>
|
<Card sx={{ margin: '10px' }}>
|
||||||
<CardActionArea component={Link} to={`/sources/${id}/popular/?R&query=${query}`} sx={{ p: 3 }}>
|
<CardActionArea
|
||||||
|
component={Link}
|
||||||
|
to={`/sources/${id}/popular/?R&query=${searchString}`}
|
||||||
|
sx={{ p: 3 }}
|
||||||
|
>
|
||||||
<Typography variant="h5">{displayName}</Typography>
|
<Typography variant="h5">{displayName}</Typography>
|
||||||
<Typography variant="caption">{translateExtensionLanguage(lang)}</Typography>
|
<Typography variant="caption">{translateExtensionLanguage(lang)}</Typography>
|
||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
@@ -137,18 +159,23 @@ const SourceSearchPreview = ({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const SearchAll: React.FC = () => {
|
const SearchAll: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
|
const [query] = useQueryParam('query', StringParam);
|
||||||
|
const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD);
|
||||||
|
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
||||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||||
|
|
||||||
const { data: sources = [] } = requestManager.useGetSourceList();
|
const { data: sources = [] } = requestManager.useGetSourceList();
|
||||||
const [sourceToLoadingStateMap, setSourceToLoadingStateMap] = useState<SourceToLoadingStateMap>(new Map());
|
const [sourceToLoadingStateMap, setSourceToLoadingStateMap] = useState<SourceToLoadingStateMap>(new Map());
|
||||||
|
const debouncedSourceToLoadingStateMap = useDebounce(sourceToLoadingStateMap, 500);
|
||||||
|
|
||||||
const sourcesSortedByName = useMemo(() => [...sources].sort(compareSourceByName), [sources]);
|
const sourcesSortedByName = useMemo(() => [...sources].sort(compareSourceByName), [sources]);
|
||||||
const sourcesFilteredByLang = useMemo(
|
const sourcesFilteredByLang = useMemo(
|
||||||
@@ -162,9 +189,9 @@ const SearchAll: React.FC = () => {
|
|||||||
const sourcesSortedByResult = useMemo(
|
const sourcesSortedByResult = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[...sourcesFilteredByNsfw].sort((sourceA, sourceB) =>
|
[...sourcesFilteredByNsfw].sort((sourceA, sourceB) =>
|
||||||
compareSourcesBySearchResult(sourceA, sourceB, sourceToLoadingStateMap),
|
compareSourcesBySearchResult(sourceA, sourceB, debouncedSourceToLoadingStateMap),
|
||||||
),
|
),
|
||||||
[sourcesFilteredByNsfw, sourceToLoadingStateMap],
|
[sourcesFilteredByNsfw, debouncedSourceToLoadingStateMap],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateSourceLoadingState = useCallback(
|
const updateSourceLoadingState = useCallback(
|
||||||
@@ -175,7 +202,7 @@ const SearchAll: React.FC = () => {
|
|||||||
return mapCopy;
|
return mapCopy;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[sourceToLoadingStateMap, setSourceToLoadingStateMap],
|
[setSourceToLoadingStateMap],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -208,6 +235,8 @@ const SearchAll: React.FC = () => {
|
|||||||
key={source.id}
|
key={source.id}
|
||||||
source={source}
|
source={source}
|
||||||
onSearchRequestFinished={updateSourceLoadingState}
|
onSearchRequestFinished={updateSourceLoadingState}
|
||||||
|
searchString={searchString}
|
||||||
|
emptyQuery={!query}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user