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

User interaction

User refines search (e.g., selects a filter).
2

State update

InstantSearch updates the UI state.
3

State to route

State mapping converts UI state to route state.
4

URL update

Router writes the route state to the URL.
5

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:
cleanUrlOnDispose
boolean
default:"true"
Remove query parameters from URL when InstantSearch is disposed.
writeDelay
number
default:"400"
Debounce delay in milliseconds before writing to URL.
windowTitle
function
Generate page title from route state:
windowTitle(routeState) {
  return routeState.query 
    ? `Search: ${routeState.query}`
    : 'Search';
}
parseURL
function
Custom URL parsing function.
createURL
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.

Performance considerations

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