Search state in InstantSearch represents the current state of your search UI. It includes the query, active filters, pagination, and all other user interactions with search widgets.
Understanding UiState
The UiState is a plain JavaScript object that represents the complete state of your search interface. It’s defined in /home/daytona/workspace/source/packages/instantsearch.js/src/types/ui-state.ts:42-44:
type UiState = {
[ indexId : string ] : IndexUiState ;
};
Each index in your application has its own state:
type IndexUiState = Partial <{
query : string ; // Search query
page : number ; // Current page
sortBy : string ; // Index to sort by
hitsPerPage : number ; // Results per page
refinementList : { // Multi-select facets
[ attribute : string ] : string [];
};
menu : { // Single-select facets
[ attribute : string ] : string ;
};
hierarchicalMenu : { // Hierarchical facets
[ attribute : string ] : string [];
};
range : { // Numeric ranges
[ attribute : string ] : string ;
};
toggle : { // Boolean filters
[ attribute : string ] : boolean ;
};
ratingMenu : { // Rating filters
[ attribute : string ] : number ;
};
geoSearch : { // Geographic search
boundingBox : string ;
};
configure : { // Custom parameters
[ key : string ] : any ;
};
// ... and more
}>;
Reading the current state
Access the current UI state using the getUiState() method:
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
});
// After search.start()
const currentState = search . getUiState ();
console . log ( currentState );
// {
// products: {
// query: 'laptop',
// page: 2,
// refinementList: {
// brand: ['Apple', 'Dell']
// }
// }
// }
Setting the state
Update the UI state programmatically using setUiState():
Direct state setting
search . setUiState ({
products: {
query: 'laptop' ,
refinementList: {
brand: [ 'Apple' ]
}
}
});
Functional updates
Use a function for updates based on the previous state:
search . setUiState (( prevState ) => ({
... prevState ,
products: {
... prevState . products ,
page: ( prevState . products . page || 1 ) + 1 ,
}
}));
React example
import { useInstantSearch } from 'react-instantsearch' ;
function SearchControls () {
const { setUiState } = useInstantSearch ();
const clearSearch = () => {
setUiState (( prevState ) => ({
... prevState ,
products: {
query: '' ,
}
}));
};
return < button onClick = { clearSearch } > Clear Search </ button > ;
}
Different widgets contribute different parts to the UI state. Here’s what each major widget type stores:
{
query : string ; // The search query
}
Example:
{
refinementList : {
[ attribute : string ]: string [];
};
}
Example: {
refinementList : {
brand : [ 'Apple' , 'Samsung' ],
color : [ 'Black' ]
}
}
Range (RangeSlider/RangeInput)
{
sortBy : string ; // Index name
}
Example: { sortBy : 'products_price_asc' }
{
toggle : {
[ attribute : string ]: boolean ;
};
}
Example: {
toggle : {
free_shipping : true ,
on_sale : true
}
}
{
geoSearch : {
boundingBox : string ;
};
}
Example: {
geoSearch : {
boundingBox : '48.8566,2.3522,48.8566,2.3522'
}
}
Initial UI state
Set an initial state when creating the InstantSearch instance:
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
initialUiState: {
products: {
query: 'laptop' ,
refinementList: {
brand: [ 'Apple' ],
},
page: 1 ,
},
},
});
When using routing, initialUiState will be overwritten by URL parameters. The URL state takes precedence.
Controlled state with onStateChange
For complete control over state changes, use the onStateChange callback. This makes the instance “controlled”:
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
onStateChange ({ uiState , setUiState }) {
// Intercept and modify state changes
console . log ( 'State changing to:' , uiState );
// You can validate or transform the state
const modifiedState = {
... uiState ,
products: {
... uiState . products ,
// Force a maximum page
page: Math . min ( uiState . products . page || 1 , 100 ),
},
};
// You MUST call setUiState for the state to update
setUiState ( modifiedState );
},
});
When using onStateChange, you become responsible for calling setUiState. The state will not update automatically.
Multi-index state
When using multiple indices, each index has its own state under its index ID:
const uiState = {
products: {
query: 'laptop' ,
page: 1 ,
},
categories: {
query: 'electronics' ,
refinementList: {
type: [ 'featured' ],
},
},
};
Example with index widgets:
import { index } from 'instantsearch.js/es/widgets' ;
search . addWidgets ([
index ({ indexName: 'products' }). addWidgets ([
searchBox ({ container: '#products-search' }),
]),
index ({ indexName: 'categories' }). addWidgets ([
searchBox ({ container: '#categories-search' }),
]),
]);
// Access state for specific index
const productsState = search . getUiState (). products ;
State synchronization
InstantSearch keeps UI state synchronized with search parameters:
User interaction
User interacts with a widget (e.g., types in SearchBox).
Widget updates state
Widget calls its refine function, updating the helper’s search parameters.
State to UiState
Widgets’ getWidgetUiState methods convert search parameters to UI state.
Search triggered
InstantSearch triggers a new search with updated parameters.
Results rendered
Widgets receive results and re-render.
Widgets implement two methods for state synchronization:
Converts search parameters to UI state (used for routing):
getWidgetUiState (
uiState : IndexUiState ,
{ searchParameters }
): IndexUiState {
const query = searchParameters . query || '' ;
if ( query === '' ) return uiState ;
return {
... uiState ,
query ,
};
}
Converts UI state to search parameters (used when loading from URL):
getWidgetSearchParameters (
searchParameters : SearchParameters ,
{ uiState }
): SearchParameters {
return searchParameters . setQueryParameter (
'query' ,
uiState . query || ''
);
}
Example from SearchBox connector (from /home/daytona/workspace/source/packages/instantsearch.js/src/connectors/search-box/connectSearchBox.ts:156-171):
getWidgetUiState ( uiState , { searchParameters }) {
const query = searchParameters . query || '' ;
if ( query === '' || ( uiState && uiState . query === query )) {
return uiState ;
}
return {
... uiState ,
query ,
};
},
getWidgetSearchParameters ( searchParameters , { uiState }) {
return searchParameters . setQueryParameter ( 'query' , uiState . query || '' );
}
State persistence
UI state can be persisted in several ways:
URL (Routing)
The most common approach - state is automatically synced with the URL:
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
routing: true ,
});
See the routing documentation for details.
Local Storage
Save and restore state from localStorage:
// Save state on change
search . on ( 'render' , () => {
const state = search . getUiState ();
localStorage . setItem ( 'searchState' , JSON . stringify ( state ));
});
// Restore on initialization
const savedState = localStorage . getItem ( 'searchState' );
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
initialUiState: savedState ? JSON . parse ( savedState ) : {},
});
Server state
For server-side rendering, state is serialized and hydrated:
// Server
const serverState = await getServerState ( < App /> , { renderToString });
// Client
< InstantSearchSSRProvider { ... serverState } >
< App />
</ InstantSearchSSRProvider >
See the SSR documentation for details.
Debugging state
Monitor state changes for debugging:
// Log all state changes
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
onStateChange ({ uiState , setUiState }) {
console . log ( 'State changing:' , uiState );
setUiState ( uiState );
},
});
// Or listen to render events
search . on ( 'render' , () => {
console . log ( 'Current state:' , search . getUiState ());
});
React DevTools integration:
import { useInstantSearch } from 'react-instantsearch' ;
function DebugState () {
const { uiState , setUiState } = useInstantSearch ();
// State is visible in React DevTools
return null ;
}
Best practices
Use functional updates When updating state based on previous state, use the functional form of setUiState to avoid race conditions. setUiState ( prev => ({
... prev ,
products: { ... prev . products , page: 2 }
}));
Avoid direct state mutation Never mutate the state object directly. Always create new objects. // ❌ Bad
const state = search . getUiState ();
state . products . page = 2 ;
// ✅ Good
search . setUiState ( prev => ({
... prev ,
products: { ... prev . products , page: 2 }
}));
Initialize with meaningful defaults Use initialUiState to set up a good default search experience. initialUiState : {
products : {
hitsPerPage : 20 ,
sortBy : 'products_popular'
}
}
Type your state In TypeScript, define custom UI state types for better type safety. type MyUiState = {
products : {
query ?: string ;
refinementList ?: {
brand ?: string [];
};
};
};
Common patterns
Reset all refinements
function resetSearch () {
search . setUiState (( prevState ) => ({
... prevState ,
products: {
query: prevState . products . query , // Keep the query
},
}));
}
Load a saved search
function loadSavedSearch ( savedState ) {
search . setUiState ({
products: savedState ,
});
}
Sync state between instances
const search1 = instantsearch ({ /* ... */ });
const search2 = instantsearch ({ /* ... */ });
search1 . on ( 'render' , () => {
const state = search1 . getUiState ();
search2 . setUiState ( state );
});
Routing Synchronize state with URLs
InstantSearch instance Learn about the main instance
Widgets Understand the widget system
Server-side rendering State management in SSR