Search
A headless, flexible search component for e-commerce storefronts. Supports real-time search, product and collection results, price display, and more.
Purpose
This component provides a complete search solution for e-commerce applications. It handles real-time search with debouncing, displays product and collection results, supports price rendering, and integrates seamlessly with navigation systems. Use this component when you need search functionality that works across web, mobile, and other platforms with consistent behavior and API.
Features
- Real-time search with debouncing (300ms delay)
- Autocomplete suggestions for products and collections
- Product search result display with images and pricing
- Collection search result display
- Loading and error states
- Empty state handling
- Accessibility-first design patterns
Installation
- npm
- Yarn
npm install @haus-storefront-react/search
yarn add @haus-storefront-react/search
Note: This is not a public package. Contact the Haus Tech Team for access.
API Reference
Search.Root
Root component for Search. Provides context to all child components. Must wrap all other Search components.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
term | string | undefined | No | - | Initial search term |
take | number | undefined | No | 3 | Number of results to fetch |
groupByProduct | boolean | undefined | No | true | Group results by product |
collectionId | string | undefined | No | - | Filter by collection ID |
collectionSlug | string | undefined | No | - | Filter by collection slug |
facetValueIds | Array<string> | undefined | No | - | Filter by facet value IDs |
facetValueOperator | LogicalOperator | undefined | No | - | Operator for facet value filters |
facetValueFilters | Array<FacetValueFilterInput> | undefined | No | - | Facet value filter inputs |
inStock | boolean | undefined | No | - | Filter by in-stock status |
sort | SearchResultSortParameter | undefined | No | - | Sort parameters for results |
skip | number | undefined | No | - | Number of results to skip |
children | (context: UseSearchReturn) => ReactNode | No | - | Render prop with search context |
Search.Input
Input component for the search field. Must be used within Search.Root. Automatically debounces input and triggers search.
Props
Accepts all standard <input> props plus:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
asChild | boolean | No | false | Render as child component |
__scopeSearch | Scope | No | - | Internal scope prop |
Search.Clear
Clear button for the search input. Must be used within Search.Root. Only renders when there is a search term.
Props
Accepts all standard <button> props plus:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
asChild | boolean | No | false | Render as child component |
__scopeSearch | Scope | No | - | Internal scope prop |
Search.Results
Results container for search results. Must be used within Search.Root. Provides render prop with search data.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
asChild | boolean | No | false | Render as child component |
children | (data: Partial<UseSearchFieldResponse> & { isLoading: boolean; error: Error | null }) => ReactNode | Yes | - | Render prop that receives search data |
__scopeSearch | Scope | No | - | Internal scope prop |
Accepts all standard <div> props.
Render Prop Data
| Property | Type | Description |
|---|---|---|
collections | CollectionSearchResponse[] | undefined | Collection results |
items | SearchResult[] | undefined | Product results |
totalItems | number | undefined | Total number of products matching input |
facets | GroupedFacetValues | undefined | Facet data |
isLoading | boolean | Loading state flag |
error | Error | null | Error object if request failed |
Search.ProductItem
Item component for a single product. Must be used within Search.Results.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
product | SearchResult | Yes | - | Product data to display |
children | ReactNode | No | - | Child components |
__scopeSearch | Scope | No | - | Internal scope prop |
Search.ProductImage
Image component for displaying the product image. Must be used within Search.ProductItem. Returns null if no image is available.
Props
Accepts all standard <img> props plus:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
alt | string | No | product.productName | Alt text for the image |
asChild | boolean | No | false | Render as child component |
__scopeSearch | Scope | No | - | Internal scope prop |
Search.CollectionItem
Item component for a single collection. Must be used within Search.Results.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
collection | Collection | Yes | - | Collection data to display |
children | ReactNode | No | - | Child components |
__scopeSearch | Scope | No | - | Internal scope prop |
Search.CollectionImage
Image component for displaying the collection image. Must be used within Search.CollectionItem. Returns null if no image is available.
Props
Accepts all standard <img> props plus:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
alt | string | No | collection.name | Alt text for the image |
src | string | No | collection.featuredAsset?.preview | Image source URL |
asChild | boolean | No | false | Render as child component |
__scopeSearch | Scope | No | - | Internal scope prop |
Search.Price
Price component for a product. Must be used within Search.ProductItem. Provides render prop with price data.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | (props: { price: Price; priceWithTax: Price; currencyCode: CurrencyCode; isFromPrice: boolean }) => ReactNode | No | - | Render prop that receives price data |
__scopeSearch | Scope | No | - | Internal scope prop |
Render Prop Data
| Property | Type | Description |
|---|---|---|
price | Price | Product price (may be range, single value, or number) |
priceWithTax | Price | Product price with tax |
currencyCode | CurrencyCode | Currency code for the price |
isFromPrice | boolean | Whether this is a price range (from X to Y) |
Search.Loading
Loading component for search loading state. Must be used within Search.Root. Only renders when search is in loading state.
Props
Accepts all standard <div> props plus:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
asChild | boolean | No | false | Render as child component |
children | ReactNode | No | - | Content to display while loading |
__scopeSearch | Scope | No | - | Internal scope prop |
Search.Empty
Empty state component for when no results are found. Must be used within Search.Root. Only renders when search is complete, has a search term, and no results are found.
Props
Accepts all standard <div> props plus:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
asChild | boolean | No | false | Render as child component |
children | ReactNode | No | - | Content to display when empty |
__scopeSearch | Scope | No | - | Internal scope prop |
useSearch
Hook for managing search state and performing search operations. Handles debouncing, loading states, and error handling.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
initialVariables | SearchInput | Yes | Initial search variables including term, take, groupByProduct, etc. |
Options Object
interface SearchInput {
term?: Maybe<string>
collectionId?: Maybe<string>
collectionSlug?: Maybe<string>
facetValueIds?: Maybe<Array<string>>
facetValueOperator?: Maybe<LogicalOperator>
facetValueFilters?: Maybe<Array<FacetValueFilterInput>>
groupByProduct?: Maybe<boolean>
inStock?: Maybe<boolean>
sort?: Maybe<SearchResultSortParameter>
take?: Maybe<number>
skip?: Maybe<number>
}
Returns
| Return Value | Type | Description |
|---|---|---|
data | UseSearchFieldResponse | null | The loaded search data or null while loading/error |
isLoading | boolean | Loading state flag |
error | Error | null | Error object if request failed |
variables | SearchInput | Current search variables |
setVariables | (variables: SearchInput) => void | Function to update search variables |
search | (term: string) => void | Function to update search term and trigger search |
clear | () => void | Function to clear search term and results |
Basic Usage
Simple Search Component
- React
- React Native
import { Search } from '@haus-storefront-react/search'
function MySearchComponent() {
return (
<Search.Root term='' take={5}>
{(searchContext) => (
<div>
<Search.Input placeholder='Search for products...' />
<Search.Clear>✕</Search.Clear>
<Search.Results>
{({ isLoading, items }) => {
if (isLoading) return <div>Loading...</div>
if (!items || items.length === 0)
return <div>No results found</div>
return (
<div>
{items.map((product) => (
<Search.ProductItem
key={product.productId}
product={product}
>
<Search.ProductImage alt={product.productName} />
<div>{product.productName}</div>
</Search.ProductItem>
))}
</div>
)
}}
</Search.Results>
</div>
)}
</Search.Root>
)
}
import { Text } from 'react-native'
import { Search } from '@haus-storefront-react/search'
function MySearchComponent() {
return (
<Search.Root term='' take={5}>
{(searchContext) => (
<>
<Search.Input placeholder='Search for products...' />
<Search.Clear>✕</Search.Clear>
<Search.Results>
{({ isLoading, items }) => {
if (isLoading) return <Text>Loading...</Text>
if (!items || items.length === 0) {
return <Text>No results found</Text>
}
return (
<>
{items.map((product) => (
<Search.ProductItem
key={product.productId}
product={product}
>
<Search.ProductImage alt={product.productName} />
<Text>{product.productName}</Text>
</Search.ProductItem>
))}
</>
)
}}
</Search.Results>
</>
)}
</Search.Root>
)
}
Basic Hook Usage
- React
- React Native
import { useSearch } from '@haus-storefront-react/search'
function MyComponent() {
const { data, isLoading, search } = useSearch({
term: '',
take: 5,
groupByProduct: true,
})
if (isLoading) return <div>Loading...</div>
if (!data) return <div>No data</div>
return <div>{/* Render data */}</div>
}
import { View, Text } from 'react-native'
import { useSearch } from '@haus-storefront-react/search'
function MyComponent() {
const { data, isLoading, search } = useSearch({
term: '',
take: 5,
groupByProduct: true,
})
if (isLoading) return <Text>Loading...</Text>
if (!data) return <Text>No data</Text>
return <View>{/* Render data */}</View>
}
Advanced Usage
Search with Navigation and Price Display
- React
- React Native
import { Price } from '@haus-storefront-react/common-ui'
import { Search } from '@haus-storefront-react/search'
import { Link, useNavigate } from '@tanstack/react-router'
function SearchWithNavigation() {
const navigate = useNavigate()
return (
<Search.Root term='' take={5}>
{(searchContext) => (
<div>
<Search.Input
placeholder='Search for products...'
onKeyUp={(e) => {
if (e.key === 'Enter') {
const searchTerm = e.currentTarget.value
searchContext.search(searchTerm)
navigate({ to: '/search', search: { term: searchTerm } })
}
}}
/>
<Search.Clear>✕</Search.Clear>
<Search.Loading>
<div>Loading search results...</div>
</Search.Loading>
<Search.Empty>
<div>No results found. Try a different search term.</div>
</Search.Empty>
<Search.Results>
{({ isLoading, collections, items }) => {
if (isLoading) return null
return (
<div>
{/* Show first collection only */}
{collections?.[0] && (
<Search.CollectionItem collection={collections[0]}>
<Search.CollectionImage alt={collections[0].name} />
<h4>{collections[0].name}</h4>
</Search.CollectionItem>
)}
{/* Products with navigation and price */}
{items?.map((product) => (
<Search.ProductItem
key={product.productId}
product={product}
>
<Link
to='/product/$productId'
params={{ productId: product.productId }}
>
<Search.ProductImage alt={product.productName} />
<h4>{product.productName}</h4>
<Search.Price>
{({ price, priceWithTax, currencyCode }) => (
<Price.Root
price={price}
priceWithTax={priceWithTax}
currencyCode={currencyCode}
asChild
>
<div>
<Price.Amount withCurrency />
<Price.Currency />
</div>
</Price.Root>
)}
</Search.Price>
</Link>
</Search.ProductItem>
))}
{/* Show All Results Button */}
<button
onClick={() => {
const searchTerm = searchContext.variables.term
if (searchTerm) {
navigate({
to: '/search',
search: { term: searchTerm },
})
}
}}
>
Show All Results
</button>
</div>
)
}}
</Search.Results>
</div>
)}
</Search.Root>
)
}
import { View, Text, TextInput, Pressable } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { Price } from '@haus-storefront-react/common-ui'
import { Search } from '@haus-storefront-react/search'
function SearchWithNavigation() {
const navigation = useNavigation()
return (
<Search.Root term='' take={5}>
{(searchContext) => (
<View>
<Search.Input asChild>
<TextInput
placeholder='Search for products...'
onSubmitEditing={(e) => {
const searchTerm = e.nativeEvent.text || ''
searchContext.search(searchTerm)
navigation.navigate('Search', { term: searchTerm })
}}
/>
</Search.Input>
<Search.Clear asChild>
<Pressable>
<Text>✕</Text>
</Pressable>
</Search.Clear>
<Search.Loading>
<Text>Loading search results...</Text>
</Search.Loading>
<Search.Empty>
<Text>No results found. Try a different search term.</Text>
</Search.Empty>
<Search.Results>
{({ isLoading, collections, items }) => {
if (isLoading) return null
return (
<View>
{/* Show first collection only */}
{collections?.[0] && (
<Search.CollectionItem collection={collections[0]}>
<Search.CollectionImage alt={collections[0].name} />
<Text>{collections[0].name}</Text>
</Search.CollectionItem>
)}
{/* Products with navigation and price */}
{items?.map((product) => (
<Search.ProductItem
key={product.productId}
product={product}
>
<Pressable
onPress={() =>
navigation.navigate('Product', {
productId: product.productId,
})
}
>
<Search.ProductImage alt={product.productName} />
<Text>{product.productName}</Text>
<Search.Price>
{({ price, priceWithTax, currencyCode }) => (
<Price.Root
price={price}
priceWithTax={priceWithTax}
currencyCode={currencyCode}
asChild
>
<View>
<Price.Amount withCurrency />
<Price.Currency />
</View>
</Price.Root>
)}
</Search.Price>
</Pressable>
</Search.ProductItem>
))}
{/* Show All Results Button */}
<Pressable
onPress={() => {
const searchTerm = searchContext.variables.term
if (searchTerm) {
navigation.navigate('Search', { term: searchTerm })
}
}}
>
<Text>Show All Results</Text>
</Pressable>
</View>
)
}}
</Search.Results>
</View>
)}
</Search.Root>
)
}
Integration with ProductList
- React
- React Native
import { ProductList } from '@haus-storefront-react/product-list'
import { useSearchParams } from '@tanstack/react-router'
function SearchResultsPage() {
const [searchParams] = useSearchParams()
const searchTerm = searchParams.get('term') || ''
return (
<ProductList.Root
searchInputProps={{
term: searchTerm,
take: 20,
groupByProduct: true,
}}
infinitePagination={true}
>
{({ products, isLoading, error, totalItems }) => {
if (isLoading) return <div>Loading products...</div>
if (error) return <div>Error loading products</div>
if (!products.length) return <div>No products found</div>
return (
<>
<div>
Found {totalItems} products for "{searchTerm}"
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
}}
>
{products.map((product) => (
<ProductList.Item key={product.productId} product={product}>
<ProductList.Image alt={product.productName} />
<h3>{product.productName}</h3>
<ProductList.Price>
{({ price, currencyCode }) => (
<span>
{price} {currencyCode}
</span>
)}
</ProductList.Price>
</ProductList.Item>
))}
</div>
<ProductList.Pagination.Root>
{({ canGoForward, nextPage }) => (
<button onClick={nextPage} disabled={!canGoForward}>
Load More Products
</button>
)}
</ProductList.Pagination.Root>
</>
)
}}
</ProductList.Root>
)
}
import { View, Text, Pressable, ScrollView } from 'react-native'
import { useRoute } from '@react-navigation/native'
import { ProductList } from '@haus-storefront-react/product-list'
function SearchResultsPage() {
const route = useRoute()
const searchTerm =
(route.params && (route.params as { term?: string }).term) || ''
return (
<ProductList.Root
searchInputProps={{
term: searchTerm,
take: 20,
groupByProduct: true,
}}
infinitePagination={true}
>
{({ products, isLoading, error, totalItems }) => {
if (isLoading) return <Text>Loading products...</Text>
if (error) return <Text>Error loading products</Text>
if (!products.length) return <Text>No products found</Text>
return (
<View>
<Text>
Found {totalItems} products for "{searchTerm}"
</Text>
<ScrollView>
{products.map((product) => (
<ProductList.Item key={product.productId} product={product}>
<ProductList.Image alt={product.productName} />
<Text>{product.productName}</Text>
<ProductList.Price>
{({ price, currencyCode }) => (
<Text>
{price} {currencyCode}
</Text>
)}
</ProductList.Price>
</ProductList.Item>
))}
</ScrollView>
<ProductList.Pagination.Root>
{({ canGoForward, nextPage }) => (
<Pressable onPress={nextPage} disabled={!canGoForward}>
<Text>Load More Products</Text>
</Pressable>
)}
</ProductList.Pagination.Root>
</View>
)
}}
</ProductList.Root>
)
}
Conditional Rendering Patterns
- React
- React Native
import { Search } from '@haus-storefront-react/search'
function ConditionalSearchComponent() {
return (
<Search.Root term='' take={5}>
{(searchContext) => {
if (searchContext.isLoading) {
return (
<Search.Loading>
<div>Loading...</div>
</Search.Loading>
)
}
if (searchContext.error) {
return <div>Error: {searchContext.error.message}</div>
}
if (!searchContext.data || searchContext.data.items.length === 0) {
return (
<Search.Empty>
<div>No results</div>
</Search.Empty>
)
}
return (
<div>
<Search.Input placeholder='Search...' />
<Search.Results>
{({ items }) => (
<div>
{items?.map((product) => (
<Search.ProductItem
key={product.productId}
product={product}
>
<Search.ProductImage alt={product.productName} />
<div>{product.productName}</div>
</Search.ProductItem>
))}
</div>
)}
</Search.Results>
</div>
)
}}
</Search.Root>
)
}
import { View, Text, TextInput } from 'react-native'
import { Search } from '@haus-storefront-react/search'
function ConditionalSearchComponent() {
return (
<Search.Root term='' take={5}>
{(searchContext) => {
if (searchContext.isLoading) {
return (
<Search.Loading>
<Text>Loading...</Text>
</Search.Loading>
)
}
if (searchContext.error) {
return <Text>Error: {searchContext.error.message}</Text>
}
if (!searchContext.data || searchContext.data.items.length === 0) {
return (
<Search.Empty>
<Text>No results</Text>
</Search.Empty>
)
}
return (
<View>
<Search.Input asChild>
<TextInput placeholder='Search...' />
</Search.Input>
<Search.Results>
{({ items }) => (
<View>
{items?.map((product) => (
<Search.ProductItem
key={product.productId}
product={product}
>
<Search.ProductImage alt={product.productName} />
<Text>{product.productName}</Text>
</Search.ProductItem>
))}
</View>
)}
</Search.Results>
</View>
)
}}
</Search.Root>
)
}
Made with ❤️ by Haus Tech Team