From c32cb535920da8ac3653890a76a413e225fc8f29 Mon Sep 17 00:00:00 2001
From: schroda <50052685+schroda@users.noreply.github.com>
Date: Tue, 23 May 2023 13:35:09 +0200
Subject: [PATCH] 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
---
src/screens/SearchAll.tsx | 145 +++++++++++++++++++++++---------------
1 file changed, 87 insertions(+), 58 deletions(-)
diff --git a/src/screens/SearchAll.tsx b/src/screens/SearchAll.tsx
index 3d062b4e..ebdcbed0 100644
--- a/src/screens/SearchAll.tsx
+++ b/src/screens/SearchAll.tsx
@@ -78,77 +78,104 @@ const compareSourcesBySearchResult = (
};
const TRIGGER_SEARCH_THRESHOLD = 1000; // ms
-const SourceSearchPreview = ({
- source,
- onSearchRequestFinished,
-}: {
- source: ISource;
- onSearchRequestFinished: (source: ISource, isLoading: boolean, hasResults: boolean, emptySearch: boolean) => void;
-}) => {
- const { t } = useTranslation();
- const [query] = useQueryParam('query', StringParam);
- const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD);
- const skipRequest = !searchString;
+const SourceSearchPreview = React.memo(
+ ({
+ source,
+ onSearchRequestFinished,
+ searchString,
+ emptyQuery,
+ }: {
+ source: ISource;
+ onSearchRequestFinished: (
+ source: ISource,
+ isLoading: boolean,
+ hasResults: boolean,
+ emptySearch: boolean,
+ ) => void;
+ searchString: string | null | undefined;
+ emptyQuery: boolean;
+ }) => {
+ const { t } = useTranslation();
+ const skipRequest = !searchString;
- const { id, displayName, lang } = source;
- const {
- data: searchResult,
- size,
- setSize,
- isLoading,
- error,
- } = requestManager.useSourceSearch(id, searchString ?? '', 1, { skipRequest });
- const mangas = !isLoading ? searchResult?.[0]?.mangaList ?? [] : [];
- const noMangasFound = !isLoading && !mangas.length;
+ const { id, displayName, lang } = source;
+ const {
+ data: searchResult,
+ size,
+ setSize,
+ isLoading,
+ error,
+ abortRequest,
+ } = requestManager.useSourceSearch(id, searchString ?? '', 1, { skipRequest });
+ const mangas = !isLoading ? searchResult?.[0]?.mangaList ?? [] : [];
+ const noMangasFound = !isLoading && !mangas.length;
- useEffect(() => {
- onSearchRequestFinished(source, isLoading, !noMangasFound, !searchString);
- }, [isLoading, noMangasFound, searchString]);
+ useEffect(() => {
+ onSearchRequestFinished(source, isLoading, !noMangasFound, !searchString);
+ }, [isLoading, noMangasFound, searchString]);
- if (!isLoading && !searchString) {
- return null;
- }
+ let errorMessage: string | undefined;
+ if (error) {
+ errorMessage = t('search.error.label.source_search_failed');
+ } else if (noMangasFound) {
+ errorMessage = t('manga.error.label.no_mangas_found');
+ }
- let errorMessage: string | undefined;
- if (error) {
- errorMessage = t('search.error.label.source_search_failed');
- } else if (noMangasFound) {
- errorMessage = t('manga.error.label.no_mangas_found');
- }
+ useEffect(
+ () => () => {
+ abortRequest(
+ new Error(`SourceSearchPreview(${source.id}, ${source.displayName}): search string changed`),
+ );
+ },
+ [searchString],
+ );
- return (
- <>
-
-
- {displayName}
- {translateExtensionLanguage(lang)}
-
-
-
- >
- );
-};
+ if ((!isLoading && !searchString) || emptyQuery) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ {displayName}
+ {translateExtensionLanguage(lang)}
+
+
+
+ >
+ );
+ },
+);
const SearchAll: React.FC = () => {
const { t } = useTranslation();
const { setTitle, setAction } = useContext(NavbarContext);
+ const [query] = useQueryParam('query', StringParam);
+ const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD);
+
const [shownLangs, setShownLangs] = useLocalStorage('shownSourceLangs', sourceDefualtLangs());
const [showNsfw] = useLocalStorage('showNsfw', true);
const { data: sources = [] } = requestManager.useGetSourceList();
const [sourceToLoadingStateMap, setSourceToLoadingStateMap] = useState(new Map());
+ const debouncedSourceToLoadingStateMap = useDebounce(sourceToLoadingStateMap, 500);
const sourcesSortedByName = useMemo(() => [...sources].sort(compareSourceByName), [sources]);
const sourcesFilteredByLang = useMemo(
@@ -162,9 +189,9 @@ const SearchAll: React.FC = () => {
const sourcesSortedByResult = useMemo(
() =>
[...sourcesFilteredByNsfw].sort((sourceA, sourceB) =>
- compareSourcesBySearchResult(sourceA, sourceB, sourceToLoadingStateMap),
+ compareSourcesBySearchResult(sourceA, sourceB, debouncedSourceToLoadingStateMap),
),
- [sourcesFilteredByNsfw, sourceToLoadingStateMap],
+ [sourcesFilteredByNsfw, debouncedSourceToLoadingStateMap],
);
const updateSourceLoadingState = useCallback(
@@ -175,7 +202,7 @@ const SearchAll: React.FC = () => {
return mapCopy;
});
},
- [sourceToLoadingStateMap, setSourceToLoadingStateMap],
+ [setSourceToLoadingStateMap],
);
useEffect(() => {
@@ -208,6 +235,8 @@ const SearchAll: React.FC = () => {
key={source.id}
source={source}
onSearchRequestFinished={updateSourceLoadingState}
+ searchString={searchString}
+ emptyQuery={!query}
/>
))}
>