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