Routing in InstantSearch synchronizes your search UI state with the browser URL. This enables:
Shareable searches : Users can bookmark or share search results
Browser navigation : Back/forward buttons work as expected
SEO benefits : Search engines can index different search states
Deep linking : Direct links to specific search configurations
Enabling routing
Enable routing by passing routing: true to the InstantSearch configuration:
import instantsearch from 'instantsearch.js' ;
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
routing: true ,
});
This uses default configuration with the browser’s History API and simple state mapping.
How routing works
The routing system consists of two main components defined in /home/daytona/workspace/source/packages/instantsearch.js/src/middlewares/createRouterMiddleware.ts:
Router
Handles reading from and writing to the URL:
type Router < TRouteState > = {
read : () => TRouteState ; // Read state from URL
write : ( route : TRouteState ) => void ; // Write state to URL
createURL : ( route : TRouteState ) => string ; // Generate URL for state
onUpdate : ( callback : ( route : TRouteState ) => void ) => void ; // Listen to URL changes
start ?: () => void ; // Optional initialization
dispose : () => void ; // Cleanup
};
State mapping
Converts between UI state and URL-friendly route state:
type StateMapping < TUiState , TRouteState > = {
stateToRoute : ( uiState : TUiState ) => TRouteState ; // UI state → URL
routeToState : ( routeState : TRouteState ) => TUiState ; // URL → UI state
};
User interaction
User refines search (e.g., selects a filter).
State update
InstantSearch updates the UI state.
State to route
State mapping converts UI state to route state.
URL update
Router writes the route state to the URL.
URL to state (on page load)
Router reads URL, state mapping converts to UI state, InstantSearch initializes with that state.
Configuration options
Pass a configuration object for more control:
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
routing: {
router: historyRouter ({
// Router options
}),
stateMapping: simpleStateMapping ({
// State mapping options
}),
},
});
Built-in routers
History router (default)
Uses the browser’s History API for clean URLs:
import { history } from 'instantsearch.js/es/lib/routers' ;
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
routing: {
router: history ({
// Optional: URL structure
cleanUrlOnDispose: true ,
// Optional: Debounce URL updates (ms)
writeDelay: 400 ,
// Optional: Custom URL parsing
parseURL : ({ qsModule , location }) => {
return qsModule . parse ( location . search . slice ( 1 ));
},
// Optional: Custom URL creation
createURL : ({ qsModule , routeState , location }) => {
const queryString = qsModule . stringify ( routeState );
return ` ${ location . pathname } ? ${ queryString } ` ;
},
}),
},
});
Options:
Remove query parameters from URL when InstantSearch is disposed.
Debounce delay in milliseconds before writing to URL.
Generate page title from route state: windowTitle ( routeState ) {
return routeState . query
? `Search: ${ routeState . query } `
: 'Search' ;
}
Custom URL parsing function.
Custom URL creation function.
Simple state mapping (default)
Maps UI state directly to URL parameters:
import { simple } from 'instantsearch.js/es/lib/stateMappings' ;
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
routing: {
stateMapping: simple (),
},
});
Example URL with simple mapping:
/search?products[query]=laptop&products[page]=2&products[refinementList][brand][0]=Apple
Custom state mapping
Create a custom state mapping for cleaner URLs:
const customStateMapping = {
stateToRoute ( uiState ) {
const indexUiState = uiState . products || {};
return {
q: indexUiState . query ,
page: indexUiState . page ,
brands: indexUiState . refinementList ?. brand ,
category: indexUiState . menu ?. category ,
price: indexUiState . range ?. price ,
};
},
routeToState ( routeState ) {
return {
products: {
query: routeState . q ,
page: routeState . page ,
refinementList: {
brand: routeState . brands || [],
},
menu: {
category: routeState . category ,
},
range: {
price: routeState . price ,
},
},
};
},
};
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
routing: {
stateMapping: customStateMapping ,
},
});
This produces cleaner URLs:
/search?q=laptop&page=2&brands=Apple&brands=Dell
SEO-friendly URLs
Create readable URLs using custom routing:
import { history } from 'instantsearch.js/es/lib/routers' ;
const seoStateMapping = {
stateToRoute ( uiState ) {
const indexUiState = uiState . products || {};
return {
q: indexUiState . query ,
category: indexUiState . menu ?. categories ,
page: indexUiState . page ,
};
},
routeToState ( routeState ) {
return {
products: {
query: routeState . q ,
menu: {
categories: routeState . category ,
},
page: routeState . page ,
},
};
},
};
const seoRouter = history ({
createURL ({ qsModule , routeState , location }) {
const { q , category , page } = routeState ;
// Create readable paths
if ( category && q ) {
return `/search/ ${ category } / ${ encodeURIComponent ( q ) } / ${
page ? `page- ${ page } ` : ''
} ` ;
}
if ( q ) {
return `/search/ ${ encodeURIComponent ( q ) }${
page ? `?page= ${ page } ` : ''
} ` ;
}
return '/search' ;
},
parseURL ({ location }) {
// Parse readable paths back to state
const matches = location . pathname . match ( / \/ search (?: \/ ( [ ^ \/ ] + )) ? (?: \/ ( [ ^ \/ ] + )) ? / );
if ( ! matches ) return {};
const [, categoryOrQuery , query ] = matches ;
return {
category: query ? categoryOrQuery : undefined ,
q: query || categoryOrQuery ,
page: new URLSearchParams ( location . search ). get ( 'page' ),
};
},
});
const search = instantsearch ({
indexName: 'products' ,
searchClient ,
routing: {
router: seoRouter ,
stateMapping: seoStateMapping ,
},
});
Produces URLs like:
/search/laptop
/search/electronics/laptop
/search/electronics/laptop/page-2
Multi-index routing
Handle multiple indices in URLs:
const multiIndexStateMapping = {
stateToRoute ( uiState ) {
return {
products: {
q: uiState . products ?. query ,
brands: uiState . products ?. refinementList ?. brand ,
},
articles: {
q: uiState . articles ?. query ,
tags: uiState . articles ?. refinementList ?. tags ,
},
};
},
routeToState ( routeState ) {
return {
products: {
query: routeState . products ?. q ,
refinementList: {
brand: routeState . products ?. brands || [],
},
},
articles: {
query: routeState . articles ?. q ,
refinementList: {
tags: routeState . articles ?. tags || [],
},
},
};
},
};
Framework-specific routing
Next.js App Router
Use the built-in Next.js routing:
import { InstantSearchNext } from 'react-instantsearch-nextjs' ;
export default function Search () {
return (
< InstantSearchNext
searchClient = { searchClient }
indexName = "products"
routing // Automatically uses Next.js routing
>
< SearchBox />
< Hits />
</ InstantSearchNext >
);
}
Next.js Pages Router
import { InstantSearch } from 'react-instantsearch' ;
import { useRouter } from 'next/router' ;
import { history } from 'instantsearch.js/es/lib/routers' ;
function Search () {
const router = useRouter ();
const routing = {
router: history ({
push ( url ) {
router . push ( url , undefined , { shallow: true });
},
}),
};
return (
< InstantSearch
searchClient = { searchClient }
indexName = "products"
routing = { routing }
>
{ /* widgets */ }
</ InstantSearch >
);
}
Vue Router
import { history } from 'instantsearch.js/es/lib/routers' ;
const routing = {
router: history ({
push ( url ) {
this . $router . push ( url );
},
}),
};
Preventing initial URL update
Prevent the URL from updating on the initial page load:
let isFirstLoad = true ;
const router = history ({
write ({ qsModule , routeState }) {
if ( isFirstLoad ) {
isFirstLoad = false ;
return ;
}
// Normal URL update
const url = qsModule . stringify ( routeState );
window . history . pushState ( routeState , '' , `? ${ url } ` );
},
});
Debouncing URL updates
Control how frequently the URL updates:
const router = history ({
writeDelay: 800 , // Wait 800ms before updating URL
});
This is useful for fast-changing inputs like search boxes.
URL structure examples
/search?products[query]=laptop
Default simple state mapping.
/search?q=laptop&page=2&brand=Apple
Custom state mapping with flat structure.
/search/electronics/laptop/page-2
Custom router with path-based routing.
/search?products[query]=laptop&articles[query]=reviews
Multiple indices with simple state mapping.
/search?q=laptop&filters=brand:Apple~brand:Dell~price:500:1000
Custom encoding for multiple filters.
Common patterns
Exclude specific parameters from URL
const stateMapping = {
stateToRoute ( uiState ) {
const indexUiState = uiState . products || {};
return {
q: indexUiState . query ,
page: indexUiState . page ,
// Don't include hitsPerPage in URL
};
},
routeToState ( routeState ) {
return {
products: {
query: routeState . q ,
page: routeState . page ,
hitsPerPage: 20 , // Always use default
},
};
},
};
Hash-based routing
For apps that can’t use History API:
import { history } from 'instantsearch.js/es/lib/routers' ;
const router = history ({
parseURL ({ location }) {
const hash = location . hash . slice ( 1 );
return qs . parse ( hash );
},
createURL ({ routeState }) {
const hash = qs . stringify ( routeState );
return `# ${ hash } ` ;
},
});
Prefixed URLs
Add a prefix to all search URLs:
const router = history ({
createURL ({ qsModule , routeState , location }) {
const queryString = qsModule . stringify ( routeState );
return `/app/search? ${ queryString } ` ;
},
parseURL ({ qsModule , location }) {
return qsModule . parse ( location . search . slice ( 1 ));
},
});
Debugging routing
Log routing state changes:
const debugStateMapping = {
stateToRoute ( uiState ) {
console . log ( 'UI State → Route:' , uiState );
const route = /* your mapping */ ;
console . log ( 'Route result:' , route );
return route ;
},
routeToState ( routeState ) {
console . log ( 'Route → UI State:' , routeState );
const state = /* your mapping */ ;
console . log ( 'State result:' , state );
return state ;
},
};
Best practices
Keep URLs readable Use custom state mapping to create clean, SEO-friendly URLs that users can understand and share.
Debounce typing Set a writeDelay to avoid updating the URL on every keystroke in the search box.
Handle missing state gracefully Always provide defaults when parsing route state in case parameters are missing.
Test URL parsing Ensure your routeToState correctly handles edge cases and malformed URLs.
Mind initialUiState vs routing Remember that routing state overrides initialUiState when both are present.
Use shallow routing in Next.js Prevent full page reloads by using shallow routing for URL updates.
Debounce URL writes : Use writeDelay to reduce history entries and improve performance during rapid interactions.router : history ({ writeDelay: 400 })
Avoid blocking URL updates : Ensure your stateToRoute and routeToState functions are synchronous and fast.
Search state Understanding UI state structure
InstantSearch instance Core instance configuration
Server-side rendering SSR with routing
Routing guide Complete routing implementation guide