@jungle-commerce/typesense-react
A powerful, headless React library for building search experiences with Typesense. This package provides comprehensive search state management, advanced filtering, multi-collection search, and automatic schema discovery - all without UI dependencies.
Features
- 🚀 React Hooks - Simple hooks like
useSearch
anduseAdvancedFacets
for instant search - 🔧 TypeScript First - Full type safety and IntelliSense support
- ⚡ Performance Optimized - Request debouncing, caching, and smart re-rendering
- 🎨 UI Agnostic - Works with any React UI library (Material-UI, Ant Design, etc.)
- 🔍 Advanced Search - Filtering, faceting, sorting, and geo-search out of the box
- 📱 Multi-Collection - Search across multiple collections simultaneously
- 🤖 AI Ready - Claude MCP integration for AI-powered search assistance
- 🧠 Schema Discovery - Automatically configure search based on your data
Documentation
📚 Full Documentation - Comprehensive guides, API reference, and examples
Quick Links
Getting Started
API Reference
Guides & Tutorials
Examples
- Minimal Search - Simplest possible search implementation
- Product Search - E-commerce product search with filters
- Documentation Search - Search through documentation
- Multi-Collection Search - Search across multiple collections
- Basic Search App - Basic search with TypeScript
- Advanced Filtering - Complex filtering examples
- Multi-Collection Demo - Another multi-collection example
Additional Documentation
Testing Documentation
- Testing Guide - Comprehensive testing documentation
- Integration Testing - Integration test setup
- Test Infrastructure - Test infrastructure details
Code Examples
- Feature Examples - Examples of all features
- Error Handling Examples - Error handling patterns
- Date Filtering Examples - Date filter examples
- Performance Optimization Examples - Performance tips
API Pattern Documentation
- Core Patterns - Core API patterns
- Hooks Patterns - Hook usage patterns
- Core Examples - Core API examples
- Hooks Examples - Hook examples
- Provider Examples - Provider examples
- Type Examples - TypeScript type examples
Installation
npm install @jungle-commerce/typesense-react typesense
Quick Start Examples
Connecting to Typesense
import { TypesenseConfig } from '@jungle-commerce/typesense-react';
const typesenseConfig: TypesenseConfig = {
nodes: [{
host: 'localhost',
port: 8108,
protocol: 'http'
}],
apiKey: 'your-search-api-key',
connectionTimeoutSeconds: 2,
cacheSearchResultsForSeconds: 60 // 1 minute cache
};
Retrieving a Schema
import { TypesenseSearchClient } from '@jungle-commerce/typesense-react';
const client = new TypesenseSearchClient(typesenseConfig);
const schema = await client.retrieveSchema('products');
console.log(schema);
Static/Fixed Configuration Examples
Simple Query
import React from 'react';
import { SearchProvider, useSearch } from '@jungle-commerce/typesense-react';
function App() {
return (
<SearchProvider
config={typesenseConfig}
collection="products"
searchOnMount={true}
>
<SearchInterface />
</SearchProvider>
);
}
function SearchInterface() {
const { state, actions } = useSearch();
return (
<div>
<input
value={state.query}
onChange={(e) => actions.setQuery(e.target.value)}
placeholder="Search products..."
/>
{state.loading && <p>Searching...</p>}
{state.results?.hits.map(hit => (
<div key={hit.document.id}>
<h3>{hit.document.name}</h3>
<p>{hit.document.description}</p>
</div>
))}
</div>
);
}
Filtered Query
import { SearchProvider, useSearch, useAdvancedFacets } from '@jungle-commerce/typesense-react';
function App() {
const facetConfig = [
{
field: 'category',
label: 'Category',
type: 'checkbox',
disjunctive: true // Allow multiple selections with OR logic
},
{
field: 'price',
label: 'Price Range',
type: 'numeric',
numericDisplay: 'range'
}
];
return (
<SearchProvider
config={typesenseConfig}
collection="products"
facets={facetConfig}
searchOnMount={true}
>
<FilteredSearch />
</SearchProvider>
);
}
function FilteredSearch() {
const { state } = useSearch();
const facets = useAdvancedFacets();
return (
<div>
{/* Category Filter */}
<div>
<h4>Categories</h4>
{state.results?.facet_counts?.find(f => f.field_name === 'category')?.counts.map(item => (
<label key={item.value}>
<input
type="checkbox"
checked={facets.disjunctiveFacets.category?.includes(item.value)}
onChange={() => facets.actions.toggleFacetValue('category', item.value)}
/>
{item.value} ({item.count})
</label>
))}
</div>
{/* Price Range Filter */}
<div>
<h4>Price Range</h4>
<input
type="range"
min="0"
max="1000"
value={facets.numericFilters.price?.min || 0}
onChange={(e) => facets.actions.setNumericFilter('price',
Number(e.target.value),
facets.numericFilters.price?.max || 1000
)}
/>
</div>
{/* Results */}
<div>
{state.results?.hits.map(hit => (
<div key={hit.document.id}>
<h3>{hit.document.name}</h3>
<p>Category: {hit.document.category}</p>
<p>Price: ${hit.document.price}</p>
</div>
))}
</div>
</div>
);
}
Query with Sorting
function SortedSearch() {
const { state, actions } = useSearch();
return (
<div>
<select
value={state.sortBy}
onChange={(e) => actions.setSortBy(e.target.value)}
>
<option value="">Relevance</option>
<option value="price:asc">Price: Low to High</option>
<option value="price:desc">Price: High to Low</option>
<option value="created_at:desc">Newest First</option>
</select>
{/* Results sorted according to selection */}
{state.results?.hits.map(hit => (
<div key={hit.document.id}>{hit.document.name}</div>
))}
</div>
);
}
Query with Faceting
function FacetedSearch() {
const facetConfig = [
{ field: 'brand', label: 'Brand', type: 'checkbox', disjunctive: true },
{ field: 'color', label: 'Color', type: 'select' },
{ field: 'in_stock', label: 'Availability', type: 'checkbox' }
];
return (
<SearchProvider
config={typesenseConfig}
collection="products"
facets={facetConfig}
searchOnMount={true}
enableDisjunctiveFacetQueries={true} // Enable OR logic for disjunctive facets
>
<FacetInterface />
</SearchProvider>
);
}
function FacetInterface() {
const { state } = useSearch();
const facets = useAdvancedFacets();
return (
<div>
{state.facets.map(facetConfig => {
const facetResults = state.results?.facet_counts?.find(
f => f.field_name === facetConfig.field
);
if (facetConfig.type === 'select') {
return (
<select
key={facetConfig.field}
value={facets.selectiveFilters[facetConfig.field] || ''}
onChange={(e) => facets.actions.setSelectiveFilter(
facetConfig.field,
e.target.value
)}
>
<option value="">All {facetConfig.label}</option>
{facetResults?.counts.map(item => (
<option key={item.value} value={item.value}>
{item.value} ({item.count})
</option>
))}
</select>
);
}
return (
<div key={facetConfig.field}>
<h4>{facetConfig.label}</h4>
{facetResults?.counts.map(item => (
<label key={item.value}>
<input
type="checkbox"
checked={facets.disjunctiveFacets[facetConfig.field]?.includes(item.value)}
onChange={() => facets.actions.toggleFacetValue(facetConfig.field, item.value)}
/>
{item.value} ({item.count})
</label>
))}
</div>
);
})}
</div>
);
}
Query Utilizing All Functionality
function AdvancedSearch() {
const facetConfig = [
{ field: 'category', label: 'Category', type: 'checkbox', disjunctive: true },
{ field: 'price', label: 'Price', type: 'numeric', numericDisplay: 'range' },
{ field: 'rating', label: 'Rating', type: 'numeric', numericDisplay: 'checkbox' },
{ field: 'release_date', label: 'Release Date', type: 'date' },
{ field: 'status', label: 'Status', type: 'select' }
];
return (
<SearchProvider
config={typesenseConfig}
collection="products"
facets={facetConfig}
searchOnMount={true}
accumulateFacets={true} // Remember facet values across searches
initialState={{
perPage: 24,
sortBy: 'popularity:desc',
additionalFilters: 'in_stock:true', // Native Typesense filter
multiSortBy: [ // Multi-field sorting
{ field: 'popularity', order: 'desc' },
{ field: 'price', order: 'asc' }
]
}}
>
<FullFeaturedSearch />
</SearchProvider>
);
}
function FullFeaturedSearch() {
const { state, actions } = useSearch();
const facets = useAdvancedFacets();
// Calculate derived values
const totalResults = state.results?.found || 0;
const totalPages = Math.ceil(totalResults / state.perPage);
const hasNextPage = state.page < totalPages;
const hasPreviousPage = state.page > 1;
return (
<div>
{/* Search Input */}
<input
value={state.query}
onChange={(e) => actions.setQuery(e.target.value)}
placeholder="Search..."
/>
{/* Additional Filters */}
<button onClick={() => actions.setAdditionalFilters('featured:true && discount:>0')}>
Show Featured & Discounted
</button>
{/* Multi-field Sort */}
<button onClick={() => actions.setMultiSortBy([
{ field: 'rating', order: 'desc' },
{ field: 'price', order: 'asc' },
{ field: 'name', order: 'asc' }
])}>
Sort by Rating, then Price, then Name
</button>
{/* All Facets */}
<div>
<h3>Filters ({facets.activeFilterCount} active)</h3>
{/* Render all configured facets */}
{/* ... facet rendering code ... */}
<button onClick={() => facets.actions.clearAllFilters()}>
Clear All Filters
</button>
</div>
{/* Results */}
<div>
<p>Found {totalResults} results (Page {state.page} of {totalPages})</p>
{state.results?.hits.map(hit => (
<div key={hit.document.id}>
{/* Highlighted fields */}
<h3 dangerouslySetInnerHTML={{
__html: hit.highlight?.name?.snippet || hit.document.name
}} />
</div>
))}
{/* Pagination */}
<button
onClick={() => actions.setPage(state.page - 1)}
disabled={!hasPreviousPage}
>
Previous
</button>
<button
onClick={() => actions.setPage(state.page + 1)}
disabled={!hasNextPage}
>
Next
</button>
</div>
</div>
);
}
Schema Discovery Examples
Discovery with Specific Field Names
import { useSchemaDiscovery, SearchProvider } from '@jungle-commerce/typesense-react';
function SchemaBasedSearch() {
const { schema, facetConfigs, searchableFields, sortableFields } = useSchemaDiscovery({
// Exclude certain fields from faceting
excludeFields: ['internal_id', 'created_by'],
maxFacets: 10,
includeNumericFacets: true,
includeDateFacets: true,
onSchemaLoad: (schema) => {
console.log('Schema loaded:', schema);
}
});
if (!schema) return <div>Loading schema...</div>;
return (
<SearchProvider
config={typesenseConfig}
collection="products"
facets={facetConfigs} // Auto-generated facet config
initialSearchParams={{
query_by: searchableFields.join(','), // Auto-detected searchable fields
sort_by: sortableFields[0]?.field // First sortable field
}}
>
<SearchInterface />
</SearchProvider>
);
}
Discovery with Pattern Matching
function PatternBasedDiscovery() {
const { schema, facetConfigs } = useSchemaDiscovery({
// Exclude fields by pattern
patterns: {
excludePatterns: [
{ pattern: 'internal', matchType: 'contains' },
{ pattern: '_id', matchType: 'endsWith' }
]
},
// Other configuration
maxFacets: 8,
includeNumericFacets: true,
// Override specific field types
facetOverrides: {
price: { type: 'numeric', numericDisplay: 'range' },
status: { type: 'select' },
created_at: { type: 'date' }
}
});
return (
<SearchProvider
config={typesenseConfig}
collection="products"
facets={facetConfigs}
>
<SearchInterface />
</SearchProvider>
);
}
Dynamic Queries with Unknown Fields
Simple Dynamic Query
function DynamicSearch() {
const { schema, facetConfigs, searchableFields } = useSchemaDiscovery();
if (!schema) return <div>Discovering schema...</div>;
return (
<SearchProvider
config={typesenseConfig}
collection="products"
facets={facetConfigs}
initialSearchParams={{
query_by: searchableFields.join(',') || '*'
}}
searchOnMount={true}
>
<GenericSearchInterface />
</SearchProvider>
);
}
function GenericSearchInterface() {
const { state, actions } = useSearch();
return (
<div>
<input
value={state.query}
onChange={(e) => actions.setQuery(e.target.value)}
placeholder={`Search in ${state.schema?.fields.length} fields...`}
/>
{state.results?.hits.map(hit => (
<div key={hit.document.id}>
{/* Dynamically render all fields */}
{Object.entries(hit.document).map(([key, value]) => (
<p key={key}><strong>{key}:</strong> {JSON.stringify(value)}</p>
))}
</div>
))}
</div>
);
}
Dynamic Filtered Query
function DynamicFilteredSearch() {
const { schema, facetConfigs } = useSchemaDiscovery({
// Auto-detect facetable fields
maxFacets: 15,
includeNumericFacets: true,
includeDateFacets: true
});
return (
<SearchProvider
config={typesenseConfig}
collection="products"
facets={facetConfigs}
>
<DynamicFilters />
</SearchProvider>
);
}
function DynamicFilters() {
const { state } = useSearch();
const facets = useAdvancedFacets();
return (
<div>
{state.facets.map(facetConfig => {
const facetResult = state.results?.facet_counts?.find(
f => f.field_name === facetConfig.field
);
// Render appropriate UI based on facet type
switch (facetConfig.type) {
case 'numeric':
return <NumericFacet key={facetConfig.field} config={facetConfig} />;
case 'date':
return <DateFacet key={facetConfig.field} config={facetConfig} />;
case 'select':
return <SelectFacet key={facetConfig.field} config={facetConfig} />;
default:
return <CheckboxFacet key={facetConfig.field} config={facetConfig} />;
}
})}
</div>
);
}
Dynamic Sorting
function DynamicSortedSearch() {
const { schema, sortableFields } = useSchemaDiscovery({
collection: 'products',
enabled: true
});
return (
<SearchProvider
config={typesenseConfig}
collection="products"
initialState={{
sortBy: sortableFields[0]?.field ? `${sortableFields[0].field}:desc` : ''
}}
>
<DynamicSortInterface sortableFields={sortableFields} />
</SearchProvider>
);
}
function DynamicSortInterface({ sortableFields }) {
const { state, actions } = useSearch();
return (
<div>
<select value={state.sortBy} onChange={(e) => actions.setSortBy(e.target.value)}>
<option value="">Relevance</option>
{sortableFields.map(field => (
<>
<option key={`${field.field}-asc`} value={`${field.field}:asc`}>
{field.label} (Low to High)
</option>
<option key={`${field.field}-desc`} value={`${field.field}:desc`}>
{field.label} (High to Low)
</option>
</>
))}
</select>
</div>
);
}
Dynamic Faceting
function DynamicFacetedSearch() {
const { schema, facetConfigs } = useSchemaDiscovery({
// Maximum facets to generate
maxFacets: 10,
includeNumericFacets: true,
includeDateFacets: true,
// Custom type overrides
facetOverrides: {
status: { type: 'select' },
type: { type: 'select' },
created_at: { type: 'date' },
updated_at: { type: 'date' }
}
});
return (
<SearchProvider
config={typesenseConfig}
collection="products"
facets={facetConfigs}
accumulateFacets={true}
>
<AutoFacetInterface />
</SearchProvider>
);
}
Dynamic Query with All Features
function FullDynamicSearch() {
const { schema, facetConfigs, searchableFields, sortableFields } = useSchemaDiscovery({
maxFacets: 20,
includeNumericFacets: true,
includeDateFacets: true
});
// Derive field groups from schema
const fieldGroups = React.useMemo(() => {
if (!schema?.fields) return { numericFields: [], booleanFields: [] };
return {
numericFields: schema.fields
.filter(f => ['int32', 'int64', 'float'].includes(f.type))
.map(f => f.name),
booleanFields: schema.fields
.filter(f => f.type === 'bool')
.map(f => f.name)
};
}, [schema]);
return (
<SearchProvider
config={typesenseConfig}
collection="products"
facets={facetConfigs}
searchOnMount={true}
accumulateFacets={true}
initialSearchParams={{
query_by: searchableFields.join(','),
sort_by: sortableFields[0]?.field
}}
initialState={{
// Dynamic multi-sort based on field types
multiSortBy: [
// First by any boolean "featured" field
...fieldGroups.booleanFields
.filter(f => f.includes('featured'))
.map(f => ({ field: f, order: 'desc' as const })),
// Then by any numeric "score" or "rating" field
...fieldGroups.numericFields
.filter(f => f.includes('score') || f.includes('rating'))
.map(f => ({ field: f, order: 'desc' as const })),
// Finally by name
{ field: 'name', order: 'asc' as const }
]
}}
>
<DynamicSearchInterface />
</SearchProvider>
);
}
Autocomplete Implementation
Simple Autocomplete
function Autocomplete() {
const [suggestions, setSuggestions] = useState([]);
const [query, setQuery] = useState('');
return (
<SearchProvider
config={typesenseConfig}
collection="products"
searchOnMount={false}
>
<AutocompleteInput
value={query}
onChange={setQuery}
suggestions={suggestions}
onSuggestionsChange={setSuggestions}
/>
</SearchProvider>
);
}
function AutocompleteInput({ value, onChange, suggestions, onSuggestionsChange }) {
const { state, actions } = useSearch();
React.useEffect(() => {
if (state.results && !state.loading) {
// Extract suggestions from results
const newSuggestions = state.results.hits.slice(0, 5).map(hit => ({
id: hit.document.id,
text: hit.document.name,
highlight: hit.highlight?.name?.snippet
}));
onSuggestionsChange(newSuggestions);
}
}, [state.results, state.loading, onSuggestionsChange]);
React.useEffect(() => {
if (value.length >= 2) {
actions.setQuery(value);
} else {
onSuggestionsChange([]);
}
}, [value, actions, onSuggestionsChange]);
return (
<div className="autocomplete">
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="Start typing..."
/>
{suggestions.length > 0 && (
<div className="suggestions">
{suggestions.map(suggestion => (
<div
key={suggestion.id}
onClick={() => onChange(suggestion.text)}
dangerouslySetInnerHTML={{
__html: suggestion.highlight || suggestion.text
}}
/>
))}
</div>
)}
</div>
);
}
Search-as-you-type Implementation
function SearchAsYouType() {
return (
<SearchProvider
config={typesenseConfig}
collection="products"
searchOnMount={false}
initialSearchParams={{
query_by: 'name,description',
prefix: true, // Enable prefix search
num_typos: 2, // Allow typos
min_len_1typo: 4, // Minimum length for 1 typo
min_len_2typo: 7 // Minimum length for 2 typos
}}
>
<InstantSearch />
</SearchProvider>
);
}
function InstantSearch() {
const { state, actions } = useSearch();
return (
<div>
<input
value={state.query}
onChange={(e) => actions.setQuery(e.target.value)}
placeholder="Search instantly..."
autoFocus
/>
{state.query && (
<div className="instant-results">
{state.loading && <div className="typing-indicator">Searching...</div>}
{!state.loading && state.results?.found === 0 && (
<div>No results for "{state.query}"</div>
)}
{state.results?.hits.map(hit => (
<div key={hit.document.id} className="instant-result">
<strong dangerouslySetInnerHTML={{
__html: hit.highlight?.name?.snippet || hit.document.name
}} />
<small dangerouslySetInnerHTML={{
__html: hit.highlight?.description?.snippet || hit.document.description
}} />
</div>
))}
</div>
)}
</div>
);
}
Multi-Collection Search
Basic Multi-Collection Search
import { MultiCollectionProvider, useMultiCollectionContext } from '@jungle-commerce/typesense-react';
function MultiSearch() {
const collections = [
{
collection: 'products',
queryBy: 'name,description,brand',
weight: 2.0, // Products are twice as important
maxResults: 20
},
{
collection: 'categories',
queryBy: 'name,description',
weight: 1.0,
maxResults: 5
},
{
collection: 'brands',
queryBy: 'name',
weight: 1.0,
maxResults: 5
}
];
return (
<MultiCollectionProvider
config={typesenseConfig}
defaultCollections={collections}
>
<UnifiedSearch />
</MultiCollectionProvider>
);
}
function UnifiedSearch() {
const { state, search, setQuery } = useMultiCollectionContext();
const handleSearch = (query: string) => {
setQuery(query);
search({
query,
mergeStrategy: 'relevance', // Merge by relevance score
normalizeScores: true, // Normalize scores across collections
resultMode: 'interleaved' // Mix results together
});
};
return (
<div>
<input
value={state.query}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search everything..."
/>
{state.results?.hits.map(hit => (
<div key={`${hit._collection}-${hit.document.id}`}>
<span className="collection-badge">{hit._collection}</span>
<h3>{hit.document.name || hit.document.title}</h3>
<p>Score: {hit._normalizedScore.toFixed(2)}</p>
</div>
))}
</div>
);
}
Multi-Collection with Schema Discovery
function DynamicMultiSearch() {
const [collections, setCollections] = useState([]);
// Discover collections dynamically
useEffect(() => {
async function discoverCollections() {
const client = new TypesenseSearchClient(typesenseConfig);
const allCollections = await client.collections().retrieve();
// Configure each collection based on its schema
const configured = await Promise.all(
allCollections.map(async (col) => {
const schema = await client.collections(col.name).retrieve();
const searchableFields = schema.fields
.filter(f => f.type === 'string' || f.type === 'string[]')
.map(f => f.name);
return {
collection: col.name,
queryBy: searchableFields.join(','),
weight: col.name === 'products' ? 2.0 : 1.0,
maxResults: 10
};
})
);
setCollections(configured);
}
discoverCollections();
}, []);
if (collections.length === 0) return <div>Loading collections...</div>;
return (
<MultiCollectionProvider
config={typesenseConfig}
defaultCollections={collections}
>
<DynamicUnifiedSearch />
</MultiCollectionProvider>
);
}
Multi-Collection Autocomplete
function MultiCollectionAutocomplete() {
const collections = [
{
collection: 'products',
queryBy: 'name,sku',
maxResults: 3,
namespace: 'product'
},
{
collection: 'categories',
queryBy: 'name,path',
maxResults: 2,
namespace: 'category'
},
{
collection: 'brands',
queryBy: 'name',
maxResults: 2,
namespace: 'brand'
}
];
return (
<MultiCollectionProvider
config={typesenseConfig}
defaultCollections={collections}
searchOptions={{
debounceMs: 150,
searchOnMount: false
}}
>
<MultiAutocomplete />
</MultiCollectionProvider>
);
}
function MultiAutocomplete() {
const { state, search, setQuery } = useMultiCollectionContext();
const [isOpen, setIsOpen] = useState(false);
const handleInputChange = (value: string) => {
setQuery(value);
if (value.length >= 2) {
search({
query: value,
resultMode: 'perCollection', // Group by collection
perCollectionLimit: 5
});
setIsOpen(true);
} else {
setIsOpen(false);
}
};
return (
<div className="multi-autocomplete">
<input
value={state.query}
onChange={(e) => handleInputChange(e.target.value)}
placeholder="Search products, categories, brands..."
/>
{isOpen && state.results?.hitsByCollection && (
<div className="suggestions-panel">
{Object.entries(state.results.hitsByCollection).map(([collection, hits]) => (
<div key={collection} className="collection-group">
<h4>{collection}</h4>
{hits.map(hit => (
<div
key={hit.document.id}
className="suggestion"
onClick={() => {
// Handle selection based on namespace
console.log(`Selected ${hit._namespace}: ${hit.document.name}`);
setIsOpen(false);
}}
>
{hit.highlight?.name?.snippet || hit.document.name}
</div>
))}
</div>
))}
</div>
)}
</div>
);
}
Multi-Collection Search-as-you-type
function MultiCollectionInstantSearch() {
const collections = [
{
collection: 'products',
queryBy: 'name,description',
weight: 2.0,
includeFields: 'id,name,price,image',
maxResults: 10
},
{
collection: 'help_articles',
queryBy: 'title,content',
weight: 1.0,
includeFields: 'id,title,excerpt',
maxResults: 5
}
];
return (
<MultiCollectionProvider
config={typesenseConfig}
defaultCollections={collections}
searchOptions={{
debounceMs: 100,
minQueryLength: 2
}}
>
<InstantMultiSearch />
</MultiCollectionProvider>
);
}
function InstantMultiSearch() {
const { state, search, setQuery, clearResults } = useMultiCollectionContext();
useEffect(() => {
if (state.query.length >= 2) {
search({
query: state.query,
enableHighlighting: true,
highlightConfig: {
startTag: '<mark>',
endTag: '</mark>'
},
// Use round-robin to ensure variety
mergeStrategy: 'roundRobin',
globalMaxResults: 20
});
} else {
clearResults();
}
}, [state.query]);
return (
<div className="instant-multi-search">
<input
value={state.query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Start typing to search..."
className="search-input"
/>
<div className="results-container">
{state.loading && <div className="loading">Searching...</div>}
{state.results?.hits.map(hit => (
<div
key={`${hit._collection}-${hit.document.id}`}
className={`result-item ${hit._collection}`}
>
<div className="result-header">
<span className="collection-label">{hit._collection}</span>
<span className="relevance-score">
{(hit._normalizedScore * 100).toFixed(0)}% match
</span>
</div>
{hit._collection === 'products' ? (
<ProductResult hit={hit} />
) : (
<ArticleResult hit={hit} />
)}
</div>
))}
{state.results && state.results.totalFound === 0 && (
<div className="no-results">
No results found for "{state.query}"
</div>
)}
</div>
</div>
);
}
function ProductResult({ hit }) {
return (
<div className="product-result">
{hit.document.image && (
<img src={hit.document.image} alt={hit.document.name} />
)}
<div>
<h3 dangerouslySetInnerHTML={{
__html: hit.highlight?.name?.snippet || hit.document.name
}} />
<p className="price">${hit.document.price}</p>
</div>
</div>
);
}
function ArticleResult({ hit }) {
return (
<div className="article-result">
<h3 dangerouslySetInnerHTML={{
__html: hit.highlight?.title?.snippet || hit.document.title
}} />
<p dangerouslySetInnerHTML={{
__html: hit.highlight?.excerpt?.snippet || hit.document.excerpt
}} />
</div>
);
}
Previous Documentation
The following sections contain additional advanced features and implementation details:
Advanced Features
- Advanced Faceting - Multiple filter types with disjunctive support
- UI State Management - Built-in facet UI state handling
- Advanced Filtering - Raw Typesense filter support
- Schema Intelligence - Auto-configuration from schema
- Performance Optimization - Caching and query optimization
- URL State Management - Shareable search URLs
Advanced Faceting
Configure different facet types based on your data:
const facetConfig = [
{
field: 'category',
label: 'Category',
type: 'checkbox', // Multi-select checkboxes
disjunctive: true, // OR logic between selections
maxValues: 10, // Maximum values to show
searchable: true, // Enable search within facet
sortBy: 'count' // Sort by count or value
},
{
field: 'price',
label: 'Price',
type: 'numeric', // Numeric range filter
numericDisplay: 'range', // 'checkbox' | 'range' | 'both'
rangeStep: 10 // Step size for range slider
},
{
field: 'created_at',
label: 'Date Added',
type: 'date', // Date range picker
dateFormat: 'YYYY-MM-DD'
},
{
field: 'status',
label: 'Status',
type: 'select' // Single-select dropdown
},
{
field: 'tags',
label: 'Tags',
type: 'custom', // Custom filter type
renderLabel: (value) => value.toUpperCase()
}
];
UI State Management
The useFacetState
hook manages UI state for facets:
// Global facet UI state
const facetUI = useFacetState();
// Search within facet values
facetUI.setFacetSearch('category', 'elec');
const filteredValues = values.filter(v =>
v.value.toLowerCase().includes(facetUI.getFacetSearch('category'))
);
// Manage facet expansion
facetUI.toggleFacetExpanded('category');
const isExpanded = facetUI.isFacetExpanded('category');
// Track scroll position
facetUI.setFacetScrollTop('category', 100);
const scrollTop = facetUI.getFacetScrollTop('category');
Advanced Filtering and Sorting
The package now supports native Typesense filter_by strings and multi-field sorting:
// Add raw Typesense filters
actions.setAdditionalFilters('in_stock:true && (category:electronics || category:computers)');
// Multi-field sorting
actions.setMultiSortBy([
{ field: 'price', order: 'desc' },
{ field: 'rating', order: 'desc' },
{ field: 'name', order: 'asc' }
]);
// Or use in initial state
<SearchProvider
initialState={{
additionalFilters: 'featured:true && discount:>0',
multiSortBy: [
{ field: 'popularity', order: 'desc' },
{ field: 'price', order: 'asc' }
]
}}
>
Performance Optimization
<SearchProvider
config={config}
collection="products"
performanceMode={true} // Disable expensive features
enableDisjunctiveFacetQueries={false} // Disable parallel facet queries
accumulateFacets={false} // Disable facet accumulation
>
Utility Functions
Build complex filter strings programmatically:
import {
buildDisjunctiveFilter,
buildNumericFilter,
buildDateFilter,
combineFilters,
buildMultiSortString,
parseSortString
} from '@jungle-commerce/typesense-react';
// Build filters
const categoryFilter = buildDisjunctiveFilter('category', ['Electronics', 'Books']);
const priceFilter = buildNumericFilter('price', 10, 100);
const combined = combineFilters([categoryFilter, priceFilter]);
// Build sort strings
const sortString = buildMultiSortString([
{ field: 'price', order: 'desc' },
{ field: 'name', order: 'asc' }
]); // Returns: "price:desc,name:asc"
API Reference
For detailed API documentation, see the API Reference section in the original documentation above.
Testing
Quick Start
# Run unit tests
pnpm test
# Run integration tests (requires Docker)
pnpm test:integration
# Run all tests with coverage
pnpm test:all:coverage
Test Setup
- Install dependencies:
pnpm install
- For integration tests: Ensure Docker is installed and running
- Run tests: Use the commands above
For detailed testing documentation, see our Testing Guide.
Testing Your Implementation
When using typesense-react in your application:
import { SearchProvider } from '@jungle-commerce/typesense-react';
import { render } from '@testing-library/react';
const renderWithSearch = (ui: React.ReactElement) => {
return render(
<SearchProvider config={mockConfig} collection="test">
{ui}
</SearchProvider>
);
};
// Test your components
it('should search when typing', async () => {
renderWithSearch(<YourSearchComponent />);
// ... test implementation
});
Claude Integration
This package includes comprehensive Claude MCP (Model Context Protocol) integration documentation:
Testing
# Run unit tests
pnpm test
# Run integration tests (requires Docker)
pnpm test:integration
# Run all tests with coverage
pnpm test:all:coverage
For detailed testing documentation, see our Testing Guide.
Contributing
We welcome contributions! Please see our Contributing Guide.
Support
License
MIT © Jungle Commerce