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
- Promise of an Object type AutocompleteResponse
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"
/>
<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