React-instantsearch: Feature Request - Ability to prevent search on init

Created on 23 Mar 2018  ·  28Comments  ·  Source: algolia/react-instantsearch

By default <InstantSearch> performs search with an empty query on init.

Feature: What is your use case for such a feature?
Performance and network optimization. Currently page doesn't finish loading until this initial search is finished. Also, I want to prevent searches with less than a few characters.

Feature: What is your proposed API entry? The new option to add? What is the behavior?

This is available in instantsearch.js with the searchFunction hook. Docs: https://community.algolia.com/instantsearch.js/v1/documentation/#hide-results-on-init

I'd love if that hook can be used via a prop on <InstantSearch>. Ideally, every possible option for instantsearch.js would automatically be usable on <InstantSearch>.

Thank you!

❄️❄️ medium Feedback Feature request ♨ API

Most helpful comment

For anyone coming here, the solution now lies in the documentation:

let firstLoad = true;

const algoliaClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const searchClient = {
  search(requests) {
    if (firstLoad === true) {
      firstLoad = false;
      return;
    }
    return algoliaClient.search(requests);
  },
};

Thanks @Haroenv for the doc link

All 28 comments

Note that doing an initial search is actually beneficial for future network connections, since that way the first actual search that a user does is on a warm connection (no need for dns resolution, ssl handshake etc.) and will be faster than if it would be the actual first request.

What do you exactly mean with "page will not finish loading", I'm not sure if I've seen that behaviour before.

Thanks!

@Haroenv it's beneficial for future network connections if you assume that the user will use the search but that's not always the case. It's the same idea as code splitting - only use the resource when you actually need it!

I'd also be very happy not to see those empty requests when the page is first rendered.

You can use a workaround to avoid the initial request, in case the search is not the main component of your page. The requests are driven by the widgets mounted on the page. On the initial render you can display a regular input (not tied to React InstantSearch). Then when the user wants to use the search (e.g. the user focus the regular input) you can mount the React InstantSearch widgets and remove the regular one. Note that you need to restore the focus on the mounted element. This solution might now work for all use cases.

Here is an example that shows how it can be achieve. We used the onFocus event in the example to mount the widgets but you can use different strategies (like mouse proximity).

What use is there to the user to be presented with these empty-string search results? If it is just done to warm up network connections, why is it necessary to show results?

@puckey, the network connection is one of the reasons, the other is use cases like in our demos where you'd have a specific search page that shows global popular results on empty query. You can hide the hits like this if that's the interface you'd like.

Does this also avoid the initial empty-string search from happening? If not, what could I do to achieve that?

No it doesn't prevent that search from happening, what's the effect you're seeing from this search being done?

I already have a bunch of network connections being made which are more important and I run a pretty busy site which tends to go viral every now and again. I would like to avoid hitting the QPS limit when that occurs.

I can also imagine that my algolia analytics will better reflect how my users are using search (if empty string searches are being counted).

Yes that makes sense. The best way to go (if you're not displaying the empty searches at all) is to do the following:

  1. make a custom search client
  2. make the search method not call Algolia, while the other ones do

this would look roughly like this:

import algoliasearch from 'algoliasearch';

const algoliaClient = algoliasearch('yourAppId', 'yourApiKey', {
  _useRequestCache: true,
});

const searchClient = {
  search(requests) {
    const shouldSearch = requests.some(({ params: { query }}) => query !== '');
    if (shouldSearch) {
      return algoliaClient.search(requests);
    }
    return Promise.resolve({
      results: [{ hits: [] }],
    });
  },
  searchForFacetValues: algoliaClient.searchForFacetValues,
};

Note that if you _are_ displaying things when users have an empty search query, you should change that check (shouldSearch) to something more relevant.

You use searchClient as a prop on <InstantSearch />

Thanks, very helpful!

I do still feel this is worth adding as a prop to the SearchBox component. Something like initialSearch={false} or ignoreEmptyQuery.

Thanks, very helpful!

I do still feel this is worth adding as a prop to the SearchBox component. Something like initialSearch={false} or ignoreEmptyQuery.

Regarding the ignoreEmptyQuery, how would it work if I type out a word and backspace to an empty string?

@petermiles This is the tricky part for this option it's either we do not trigger a new query for all the empty query but it's not something that we want. Or we only disabled the first empty query but users might want different behaviour than this. It's not so simple to find a solution that will fit every use cases. You can find more information about this inside the PR review.

@petermiles @samouss With ignoreEmptyQuery, when you backspace to empty string, I would expect to see no results.

This is a guide we have written for Vue InstantSearch, which also works for React InstantSearch on the topic: https://v2--vue-instantsearch.netlify.com/advanced/conditional-requests.html

Replace line

const shouldSearch = requests.some(({ query }) => query !== '');

with
const shouldSearch = requests.some(({ params: { query } }) => query !== '');
if you use latest version of algolia client in the comment https://github.com/algolia/react-instantsearch/issues/1111#issuecomment-414239919

updated, thanks for catching @goran-paunovic

For anyone coming here, the solution now lies in the documentation:

let firstLoad = true;

const algoliaClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const searchClient = {
  search(requests) {
    if (firstLoad === true) {
      firstLoad = false;
      return;
    }
    return algoliaClient.search(requests);
  },
};

Thanks @Haroenv for the doc link

I have the same issue, but can't wrap my head around how to solve it, even with the provided example.

I have <InstantSearch> wrapping a Leaflet map component.

I need to defer searching until after Leaflet has initialized and the map position is set, as my search needs to be based on the map bounds.

Any thoughts of how might I achieve this, with the Map (and its load event, from which I need to "enable search") being a child component?

@timkelty, could you intitialise the map first, and only set a boolean like "showInstantSearch" render the whole InstantSearch?

Another option is to also set a variable, but it only preventing the search from actually hitting Algolia like in the last code sample

Do you have a sandbox we can try out to see how your data flow is?

@Haroenv Here's a distilled sandbox example: https://codepen.io/timkelty/pen/XWbdGxj

If you look in the console, you'll notice 2 algolia requests logged on load: one made immediately, one made after the geoSearch has been refined to the mapBounds. I only want the latter to happen.

For this specific example, I believe I could get what I'm after by filtering the requests to those that had params.insideBoundingBox…but this is a distilled example, and that won't work for me in many other case…e.g. sometimes the search is refined other ways, but I still don't want to do any searching until my map bounds are set.

It seems like a typical "lifting up state" React issue, but I can't seem to wrap my head around it in combination with the <InstantSearch> container.

Solution I see here is twofold @timkelty:

  1. filter based on the parameter, since indeed you only want to search for "refined" values
const geoSearchClient = {
  search(requests) {
    if (requests.some(req => req.params.insideBoundingBox)) {
      return searchClient.search(requests);
    }

    // since we don't display a non-geosearch query
    return {}
  },
};
  1. display hits in render, not in componentDidMount, since that way you're always a render behind:
  render() {
    return (
      <LeafletMap
        ref={this._mapRef}
        onMoveEnd={this._handleMoveEnd}
        {...this.props}
      >
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url='https://{s}.tile.osm.org/{z}/{x}/{y}.png'
        />

        {hitsToMarkers(this.props.hits).map(({key, ...marker}) => {
          return <Marker key={key} {...marker} />;
        })}
      </LeafletMap>
    );
  }

pen: https://codepen.io/Haroenv/pen/xxGOPEZ?editors=0011

if you use another widget, which would try to read a value from the result, and it errors, you need to give that value in the "mock" response. Since neither hits or our markers requires it, it's fine like this.

Thanks @Haroenv!

display hits in render, not in componentDidMount, since that way you're always a render behind:

You meant componentDidUpdate, right?

Your example misses one crucial bit to what I'm doing in mine: The displayed markers need to be _cumulative_. That is, with every move of the map, I feed in new markers from the algolia req, but don't get rid of the old ones.

In my actual example I'm getting the max of 1000 hits at a time, then using marker clustering to display many many thousands of markers 😬.

Here's the vanilla algolia-js version of what I'm rebuilding in React: https://paddling.com/paddle/locations

since that way you're always a render behind

That's a good point, here's an updated example that is closer to what I'm actually doing: https://codepen.io/timkelty/pen/mdJEXXE

I moved things out of componentDidUpdate into render, and implemented clustering.

This indeed works. It's a little unidiomatic, and might work better in getDerivedStateFromProps, but essentially it's the same.

It behaves correctly now, right @timkelty ?

It's a little unidiomatic, and might work better in getDerivedStateFromProps

Which part specifically? Are you saying I could this.addHitMarkers() in getDerivedStateFromProps instead of in render()?

It behaves correctly now, right @timkelty ?

It does what I need to for this example, yes. However, I was hoping for a more explicit/declarative way of preventing the search from inside <InstantSearch>.

E.g. what If I were able to do something like:

    <InstantSearch
      indexName='production_locations'
      searchClient={{
        search(requests, {doSearch = true, foo}) {
          if (doSearch) {
            return searchClient.search(requests);
          }

          return {};
        },
      }}
    >
      <AddSearchArg doSearch={this.state.doSearch} foo='bar' />
    </InstantSearch>

Also, I noticed you're doing requests.some, while initially I was actually request.filtering the requests…I wonder what the difference would be or if one makes more sense?

Which part specifically? Are you saying I could this.addHitMarkers() in getDerivedStateFromProps instead of in render()?

Yes, that should work too as far as I see it.

I'm not sure a doSearch boolean really fits the use case. A way to avoid it in your case, would be to make your wrapping from:

const MyMapView = connectXXX(originalMap)

<InstantSearch>
  <MyMapView />
</InstantSearch>

to something more like

const MapProvider = () => {
  useEffect(() => {
    // load the map
  })

  if (mapLoaded) {
    return children; // of course with the map itself too
  }

  return null
}

<InstantSearch>
  <MyMapView>
    {/* these will only render when the map is actually loaded,
    thus won't cause the wrong life cycle */}
    <ConnectedGeoMarkers />
  </MyMapView>
</InstantSearch>
Was this page helpful?
0 / 5 - 0 ratings

Related issues

oznekenzo picture oznekenzo  ·  3Comments

denkristoffer picture denkristoffer  ·  4Comments

developerk786 picture developerk786  ·  3Comments

aaronbushnell picture aaronbushnell  ·  4Comments

flouc001 picture flouc001  ·  5Comments