Concepts / Building Search UI / Routing urls
May. 10, 2019

Routing Urls

Overview

React InstantSearch provides the necessary API entries that enables synchronizing its state with the browser URL (among other state managers). The search state includes which widgets are refined, what is the current search query, what is the current pagination, etc.

Synchronizing your UI with the browser URL is a good practice because it makes your search experience more accessible. It makes it easy for your users to share or save a specific search. It is also a way for them to use the back and next buttons of the browser to jump to another state.

With React InstantSearch, the props that you need to use are in the InstantSearch component. This page will guide you through how to leverage this API to create multiple URL strategies based on the search experience you want to create.

By the end of this guide, you will be able to reproduce these examples:

Before we start

API Overview

searchState

This object describes the current state of the search containing all the widget states. It turns the <InstantSearch> component into a controlled component.

You can learn more on the search state guide.

onSearchStateChange(nextSearchState)

This function is called every time the search state is updated.

createURL(searchState)

This function returns a string defining the URL based on all widgets generating parameters and passed down to every connector.

Routing libraries

React InstantSearch can be plugged into any history or routing library. For any solution, you need to listen for searchState changes and inject it appropriately into the InstantSearch component.

All the examples in this tutorial use react-router.

Debouncing the URL update

A good practice is to not synchronize the search state to the browser URL too often. It could degrade the browsing experience by creating too many history entries, making the user frustrated when navigating to previous and next pages. Moreover, browsers often react badly when manipulating the URL too often.

This is why we won’t update the URL at each key stroke but rather debounce the URL update at 400 milliseconds.

Basic URL synchronization

We first will create a basic URL synchronisation without any URL transformations.

We implement the createURL() function that returns a string defining the search state based on the qs module. The searchStateToUrl() relies on this createURL() function to create the full URL. Lastly, urlToSearchState() converts the URL string back to a state object.

1
2
3
4
5
const createURL = state => `?${qs.stringify(state)}`;

const searchStateToUrl = ({location}, searchState) => searchState ? `${location.pathname}${createURL(searchState)}` : '';

const urlToSearchState = ({search}) => qs.parse(search.slice(1));

Now, we can use these functions in our React app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import React, { Component } from 'react';
import {
  InstantSearch,
  Hits,
  SearchBox,
  RefinementList,
  Pagination,
} from 'react-instantsearch-dom';
import qs from 'qs';

const DEBOUNCE_TIME = 400;
const searchClient = algoliasearch(
  'YourApplicationID',
  'YourAdminAPIKey'
);

/* `createUrl()`, `searchStateToUrl()` and `urlToSearchState()` go here */

class App extends Component {
  state = {
    searchState: urlToSearchState(this.props.location),
    lastLocation: this.props.location,
  };

  static getDerivedStateFromProps(props, state) {
    if (props.location !== state.lastLocation) {
      return {
        searchState: urlToSearchState(props.location),
        lastLocation: props.location,
      };
    }

    return null;
  }

  onSearchStateChange = searchState => {
    clearTimeout(this.debouncedSetState);

    this.debouncedSetState = setTimeout(() => {
      this.props.history.push(
        searchStateToUrl(this.props, searchState),
        searchState
      );
    }, DEBOUNCE_TIME);

    this.setState({ searchState });
  };

  render() {
    return (
      <InstantSearch
        searchClient={searchClient}
        indexName="instant_search"
        searchState={this.state.searchState}
        onSearchStateChange={this.onSearchStateChange}
        createURL={createURL}
      >
        <RefinementList attribute="brand" />

        <SearchBox className="searchbox" placeholder="Search" />
        <Hits />

        <Pagination />
      </InstantSearch>
    );
  }
}

Check out the live example to play with the routing system. When you type a and refine the brands with Sony and Samsung, you get an URL like:

1
https://website.com?query=a&page=1&refinementList[brand][0]=Samsung&refinementList[brand][1]=Sony

This is a great start, but we can make this URL more user-friendly.

User-friendly URLs

A user-friendly URL should be more readable and could look like this:

1
https://website.com?query=a&brands=Sony~Samsung&page=2

We’ll need to update the functions createURL, searchStateToUrl, urlToSearchState.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const createURL = ({query, refinementList, page}) => {
  const routeState = {
    query: query,
    brands:
      refinementList &&
      refinementList.brand &&
      refinementList.brand.join('~'),
    page: page,
  };

  return `?${qs.stringify(routeState)}`;
};

const searchStateToUrl = ({location}, searchState) => searchState ? `${location.pathname}${createURL(searchState)}` : '';

const urlToSearchState = ({search}) => {
  const routeState = qs.parse(search.slice(1));
  const searchState = {
    query: routeState.query,
    refinementList: {
      brand: routeState.brands && routeState.brands.split('~'),
    },
    page: routeState.page,
  };

  return searchState;
};

Check out the live example to play with the routing system.

You might sometimes need a few of your searches to be available from a search engine. We will see in the next section how to make these URLs more SEO-friendly.

SEO-friendly URLs

A SEO-friendly URL could store the brand in the path name and keep the query and pagination as query parameters:

1
https://website.com/search/brands/Samsung~Sony?q=a&p=1

We’ll need to update the functions createURL, searchStateToUrl, urlToSearchState.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const createURL = ({refinementList, query, page}) => {
  const brands =
    refinementList &&
    refinementList.brand &&
    refinementList.brand.join('~');

  // Reset the URL if it's the initial state
  const isDefaultRoute =
    !query &&
    page === 1 &&
    refinementList.brand &&
    refinementList.brand.length > 0;

  if (isDefaultRoute) {
    return '';
  }

  const queryParams = qs.stringify({
    q: query,
    p: page,
  });

  return `/search${brands ? `/brands/${brands}` : ''}?${queryParams}`;
};

const searchStateToUrl = (props, searchState) =>
  searchState ? createURL(searchState) : '';

const urlToSearchState = ({pathname, search}) => {
  const [, brand] = pathname.split('/brands/');
  const routeState = qs.parse(search.slice(1));

  const searchState = {
    query: routeState.q,
    refinementList: {
      brand: (brand && brand.split('~')) || [],
    },
    page: routeState.p,
  };

  return searchState;
};

Check out the live example to play with the routing system.

For your search results to be part of search engines results, you will have to selectively choose them. Trying to have all of your search results inside search engines could be considered as spam.

To do that, you can create a robots.txt at the root of your website.

Here is an example based on the URL scheme we created.

1
2
3
4
5
User-agent: *
Allow: /search/brands/Samsung
Allow: /search/brands/Apple
Disallow: /search/
Allow: *

Did you find this page helpful?