Skip to content

Commit

Permalink
Improve predictive search performance (#1823)
Browse files Browse the repository at this point in the history
* Use GET method for predictive search to avoid revalidations

* Add browser cache to predictive search API

* Add loading state to predictive search

* Changesets

* Fix type
  • Loading branch information
frandiox authored Mar 12, 2024
1 parent 351b3c1 commit c136030
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 22 deletions.
10 changes: 10 additions & 0 deletions .changeset/forty-lies-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'skeleton': patch
---

Improve performance of predictive search:

- Change the request to be GET instead of POST to avoid Remix route revalidations.
- Add Cache-Control headers to the response to get quicker results when typing.

Aside from that, it now shows a loading state when fetching the results instead of "No results found.".
19 changes: 12 additions & 7 deletions templates/skeleton/app/components/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,20 +263,18 @@ type ChildrenRenderProps = {

type SearchFromProps = {
action?: FormProps['action'];
method?: FormProps['method'];
className?: string;
children: (passedProps: ChildrenRenderProps) => React.ReactNode;
[key: string]: unknown;
};

/**
* Search form component that posts search requests to the `/search` route
* Search form component that sends search requests to the `/search` route
**/
export function PredictiveSearchForm({
action,
children,
className = 'predictive-search-form',
method = 'POST',
...props
}: SearchFromProps) {
const params = useParams();
Expand All @@ -287,13 +285,14 @@ export function PredictiveSearchForm({

function fetchResults(event: React.ChangeEvent<HTMLInputElement>) {
const searchAction = action ?? '/api/predictive-search';
const newSearchTerm = event.target.value || '';
const localizedAction = params.locale
? `/${params.locale}${searchAction}`
: searchAction;
const newSearchTerm = event.target.value || '';

fetcher.submit(
{q: newSearchTerm, limit: '6'},
{method, action: localizedAction},
{method: 'GET', action: localizedAction},
);
}

Expand Down Expand Up @@ -322,7 +321,7 @@ export function PredictiveSearchForm({
}

export function PredictiveSearchResults() {
const {results, totalResults, searchInputRef, searchTerm} =
const {results, totalResults, searchInputRef, searchTerm, state} =
usePredictiveSearch();

function goToSearchResult(event: React.MouseEvent<HTMLAnchorElement>) {
Expand All @@ -333,9 +332,14 @@ export function PredictiveSearchResults() {
window.location.href = event.currentTarget.href;
}

if (state === 'loading') {
return <div>Loading...</div>;
}

if (!totalResults) {
return <NoPredictiveSearchResults searchTerm={searchTerm} />;
}

return (
<div className="predictive-search-results">
<div>
Expand Down Expand Up @@ -452,6 +456,7 @@ function SearchResultItem({goToSearchResult, item}: SearchResultItemProps) {
type UseSearchReturn = NormalizedPredictiveSearch & {
searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
searchTerm: React.MutableRefObject<string>;
state: ReturnType<typeof useFetcher>['state'];
};

function usePredictiveSearch(): UseSearchReturn {
Expand All @@ -474,7 +479,7 @@ function usePredictiveSearch(): UseSearchReturn {
searchInputRef.current = document.querySelector('input[type="search"]');
}, []);

return {...search, searchInputRef, searchTerm};
return {...search, searchInputRef, searchTerm, state: searchFetcher.state};
}

/**
Expand Down
23 changes: 8 additions & 15 deletions templates/skeleton/app/routes/api.predictive-search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,16 @@ const DEFAULT_SEARCH_TYPES: PredictiveSearchTypes[] = [
* Fetches the search results from the predictive search API
* requested by the SearchForm component
*/
export async function action({request, params, context}: LoaderFunctionArgs) {
if (request.method !== 'POST') {
throw new Error('Invalid request method');
}

export async function loader({request, params, context}: LoaderFunctionArgs) {
const search = await fetchPredictiveSearchResults({
params,
request,
context,
});

return json(search);
return json(search, {
headers: {'Cache-Control': `max-age=${search.searchTerm ? 60 : 3600}`},
});
}

async function fetchPredictiveSearchResults({
Expand All @@ -55,15 +53,10 @@ async function fetchPredictiveSearchResults({
}: Pick<LoaderFunctionArgs, 'params' | 'context' | 'request'>) {
const url = new URL(request.url);
const searchParams = new URLSearchParams(url.search);
let body;
try {
body = await request.formData();
} catch (error) {}
const searchTerm = String(body?.get('q') || searchParams.get('q') || '');
const limit = Number(body?.get('limit') || searchParams.get('limit') || 10);
const rawTypes = String(
body?.get('type') || searchParams.get('type') || 'ANY',
);
const searchTerm = searchParams.get('q') || '';
const limit = Number(searchParams.get('limit') || 10);
const rawTypes = String(searchParams.get('type') || 'ANY');

const searchTypes =
rawTypes === 'ANY'
? DEFAULT_SEARCH_TYPES
Expand Down

0 comments on commit c136030

Please sign in to comment.