Skip to main content
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>;
}

State structure by widget

Different widgets contribute different parts to the UI state. Here’s what each major widget type stores:
{
  refinementList: {
    [attribute: string]: string[];
  };
}
Example:
{
  refinementList: {
    brand: ['Apple', 'Samsung'],
    color: ['Black']
  }
}
{
  hierarchicalMenu: {
    [attribute: string]: string[];
  };
}
Example:
{
  hierarchicalMenu: {
    categories: ['Electronics', 'Computers', 'Laptops']
  }
}
{
  range: {
    [attribute: string]: string;
  };
}
Example:
{
  range: {
    price: '10:500'
  }
}
{
  hitsPerPage: number;
}
Example:
{ hitsPerPage: 20 }
{
  sortBy: string;  // Index name
}
Example:
{ sortBy: 'products_price_asc' }
{
  toggle: {
    [attribute: string]: boolean;
  };
}
Example:
{
  toggle: {
    free_shipping: true,
    on_sale: true
  }
}
{
  ratingMenu: {
    [attribute: string]: number;
  };
}
Example:
{
  ratingMenu: {
    rating: 4
  }
}
{
  geoSearch: {
    boundingBox: string;
  };
}
Example:
{
  geoSearch: {
    boundingBox: '48.8566,2.3522,48.8566,2.3522'
  }
}
{
  configure: {
    [parameter: string]: any;
  };
}
Example:
{
  configure: {
    aroundLatLng: '48.8566, 2.3522',
    aroundRadius: 5000
  }
}

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:
1

User interaction

User interacts with a widget (e.g., types in SearchBox).
2

Widget updates state

Widget calls its refine function, updating the helper’s search parameters.
3

State to UiState

Widgets’ getWidgetUiState methods convert search parameters to UI state.
4

Search triggered

InstantSearch triggers a new search with updated parameters.
5

Results rendered

Widgets receive results and re-render.

Widget state methods

Widgets implement two methods for state synchronization:

getWidgetUiState

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,
  };
}

getWidgetSearchParameters

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
    },
  }));
}
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