Skip to main content

Autocomplete Usage

  • Implementation on file \app\routes\search.tsx
  • The next example handles both AC (Autocomplete) and SERP (Search Engine Results Page).

Configuration

getAutocompleteResults function

To customize the behavior of the getAutocompleteResults function, you can pass the specs parameter to control aspects like:

  • query - Search term (String)
  • max_products - Limit of products (Number)
  • max_collections - Limit of collections (Number)
  • max_turbolinks - Limit of turbolinks (Number)
  • max_articles - Limit of articles (Number)

Return Object

Usage Example

Imports

import {Analytics, CacheLong, Image, Money} from '@shopify/hydrogen';
import {PaginationBar, transformToShopifyStructure} from '@fast-simon/storefront-kit';
import {Narrow} from '@fast-simon/utilities';
... // additional imports

Loader Function

export async function loader({request, context}: LoaderFunctionArgs) {
const url = new URL(request.url);
const isPredictive = url.searchParams.has('predictive');
const searchPromise = isPredictive
? predictiveSearch({request, context})
: regularSearch({request, context});

searchPromise.catch((error: Error) => {
console.error(error);
return {term: '', result: null, error: error.message};
});
const data = await searchPromise;

if(data?.type === 'regular') {
const facets = {facets: data.result.getFacetsOnly ? data.result.getFacetsOnly() : data.result.facets};
const dashboardConfig = {dashboardConfig: context.fastSimon.getDashboardConfig({cacheStrategy: CacheLong()})};
return defer({...data, ...facets, ...dashboardConfig});
}
return json(data);
}

async function getFastSimonAutocompleteResults({request, context}: LoaderFunctionArgs) {
const {fastSimon} = context;
const url = new URL(request.url);
const term = String(url.searchParams.get('q') || '').trim();

return await fastSimon.getAutocompleteResults({
props: {
query: term
}
});
}

async function regularSearch({
request,
context,
}: Pick<
LoaderFunctionArgs,
'request' | 'context'
>): Promise<RegularSearchReturn> {
const {fastSimon} = context;
const url = new URL(request.url);
const term = String(url.searchParams.get('q') || '');
const page = Number(url.searchParams.get('page') || 1);
const sortBy = url.searchParams.get('sort');
const narrowString = url.searchParams.get('filters');
const narrow = narrowString ? Narrow.toServerNarrow(Narrow.parseNarrow(narrowString || '')) : []

const fastSimonSearchResults = await fastSimon.getSearchResults({
props: {
page: page,
narrow: narrow,
facetsRequired: 1,
query: term,
productsPerPage: 20,
sortBy: sortBy
}
});

if (!fastSimonSearchResults) {
throw new Error('No search data returned from FastSimon API');
}

const transformed = transformToShopifyStructure(fastSimonSearchResults.items);
const facets = fastSimonSearchResults.getFacetsOnly ? fastSimonSearchResults.getFacetsOnly() : {};
return {type: 'regular', term, error: undefined, result: {total: fastSimonSearchResults.total_results, ...fastSimonSearchResults, ...facets, items: transformed}};
}


async function predictiveSearch({
request,
context,
}: Pick<
ActionFunctionArgs,
'request' | 'context'
>): Promise<PredictiveSearchReturn> {
const url = new URL(request.url);
const term = String(url.searchParams.get('q') || '').trim();

const type = 'predictive';

if (!term) return {type, term, result: getEmptyPredictiveSearchResult()};

const {items} = await getFastSimonAutocompleteResults({request, context});

if (!items) {
throw new Error('No predictive search data returned from Shopify API');
}

const total = Object.values(items).reduce(
(acc, item) => acc + item.length,
0,
);

return {type, term, result: {items, total}};
}



... // more functions and logic

Search Page Component

export default function SearchPage() {
const {type, term, result, error, facets, dashboardConfig} = useLoaderData<typeof loader>();
if (type === 'predictive') return null;

return (
<div className="search">
<h1>Search</h1>
<SearchForm>
{({inputRef}) => (
<>
<input
defaultValue={term}
name="q"
placeholder="Search…"
ref={inputRef}
type="search"
/>
&nbsp;
<button type="submit" className={'search-button'}>Search</button>
</>
)}
</SearchForm>
{error && <p style={{color: 'red'}}>{error}</p>}
{!term || !result?.total ? (
<SearchResults.Empty />
) : (
<>
<div className={"results-filters-container"}>
<div className={'fs-products-summary'}>
<ProductsGrid products={result.items.products.nodes} />
</div>
</div>

<br />
<PaginationBar total={result.total_p} />
</>
)}
<Analytics.SearchView data={{searchTerm: term, searchResults: result}} />
</div>
);
}

function ProductsGrid({products}) {
return (
<div className="products-grid">
{products.map((product, index) => {
return (
<ProductItem
key={product.id}
product={product}
loading={index < 8 ? 'eager' : undefined}
/>
);
})}
</div>
);
}
function ProductItem({
product,
loading,
}: {
product: ProductItemFragment;
loading?: 'eager' | 'lazy';
}) {
const variant = product.variants.nodes[0];
const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
return (
<Link
className="product-item"
key={product.id}
prefetch="intent"
to={variantUrl}
>
{product.featuredImage && (
<Image
alt={product.featuredImage.altText || product.title}
aspectRatio="0.714"
data={product.featuredImage}
loading={loading}
sizes="(min-width: 45em) 400px, 100vw"
/>
)}
<h4>{product.title}</h4>
<small>
<Money data={product.priceRange.minVariantPrice} />
</small>
</Link>
);
}
... // more functions and logic