Vue-instantsearch: With Nuxt + ssr + routing, bug with the browser previous/next buttons ?

Created on 16 Apr 2021  路  11Comments  路  Source: algolia/vue-instantsearch

Hi the team,

I am trying to implement vue-instantsearch with nuxt + ssr + routing. SSR is OK.

If I refine brand attributes with "Apple" (firstly), and secondly "Samsung", and click on the browser back button (Chrome & Safari, Mac), the url is updated but not the app. (Apple and Samsung are always checked). If I hard reload the current page, Microsoft disappears.

What I Want : when I click on the browser back button, I want to have the previousState in my app (only "Apple" checked).

What I did :

  1. Create new and empty nuxt project
npx create-nuxt-app algolia-nuxt-example
cd algolia-nuxt-example
npm install vue-instantsearch algoliasearch
  1. Add search.vue in my app and follow Instructions in your doc (mix ssr nuxt implementation and nuxt routing
    /pages/search.vue
<template>
  <ais-instant-search-ssr>
    <ais-refinement-list attribute="brand" />
    <ais-hits>
      <template slot="item" slot-scope="{ item }">
        <p>
          <ais-highlight attribute="name" :hit="item" />
        </p>
        <p>
          <ais-highlight attribute="brand" :hit="item" />
        </p>
      </template>
    </ais-hits>
  </ais-instant-search-ssr>
</template>

<script>
import { AisInstantSearchSsr, AisRefinementList, AisHits, AisHighlight, createServerRootMixin } from 'vue-instantsearch';
import algoliasearch from 'algoliasearch/lite';

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

function nuxtRouter(vueRouter) {
  return {
    read() {
      return vueRouter.currentRoute.query;
    },
    write(routeState) {
      // Only push a new entry if the URL changed (avoid duplicated entries in the history)
      if (this.createURL(routeState) === this.createURL(this.read())) {
        return;
      }
      vueRouter.push({
        query: routeState,
      });
    },
    createURL(routeState) {
      return vueRouter.resolve({
        query: routeState,
      }).href;
    },
    onUpdate(cb) {
      if (typeof window === 'undefined') return;

      this._onPopState = event => {
        const routeState = event.state;
        // On initial load, the state is read from the URL without
        // update. Therefore, the state object isn't present. In this
        // case, we fallback and read the URL.
        if (!routeState) {
          cb(this.read());
        } else {
          cb(routeState);
        }
      };
      window.addEventListener('popstate', this._onPopState);
    },
    dispose() {
      if (typeof window === 'undefined') return;

      window.removeEventListener('popstate', this._onPopState);
    },
  };
}

export default {
  data() {
    // Create it in `data` to access the Vue Router
    const mixin = createServerRootMixin({
      searchClient,
      indexName: 'instant_search',
      routing: {
        router: nuxtRouter(this.$router),
      },
    });
    return {
      ...mixin.data(),
    };
  },
  provide() {
    return {
      // Provide the InstantSearch instance for SSR
      $_ais_ssrInstantSearchInstance: this.instantsearch,
    };
  },
  serverPrefetch() {
    return this.instantsearch.findResultsState(this).then(algoliaState => {
      this.$ssrContext.nuxt.algoliaState = algoliaState;
    });
  },
  beforeMount() {
    const results =
      (this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
      window.__NUXT__.algoliaState;

    this.instantsearch.hydrate(results);

    // Remove the SSR state so it can't be applied again by mistake
    delete this.$nuxt.context.nuxtState.algoliaState;
    delete window.__NUXT__.algoliaState;
  },
  components: {
    AisInstantSearchSsr,
    AisRefinementList,
    AisHits,
    AisHighlight
  },
};
</script>

nuxt.config.js

router: {
    parseQuery(queryString) {
      return require('qs').parse(queryString);
    },
    stringifyQuery(object) {
      var queryString = require('qs').stringify(object);
      return queryString ? '?' + queryString : '';
    },
  },

  build: {
    transpile: ['vue-instantsearch', 'instantsearch.js/es']
  }

Am I making an implementation error ? Thanks if you can help me, that's make me crazy!

Most helpful comment

We found a solution.

First, the problem is some kind of race-condition, i guess.
The vue-instantsearch callback inside the custom-routers onUpdate (event when doing history back) looks like this:
instantSearchInstance.setUiState(stateMapping.routeToState(route));

Problem is that the UiState got changed but the search is not triggered again.

How i solved it:
Adding a watcher $route.query that triggers the search manually.

Adding this watcher would trigger the search every time the route changes ==> also every time a refinement get's applied and put into the URL.
That would not be bad because vue-instantsearch caches the requests and does not fire them again.

Anyways, to avoid the possibility to trigger request twice, i added a dummy query-parameter when History-Back is used to only trigger the search manually when History-Back was done.

Custom-Router

onUpdate(callBack) {
      if (typeof window === 'undefined') {
        return
      }

      this._onPopState = () => {
        // this is a workaround because of a race-condition: When the UiState get's updated via the callback
        // the search is not triggered again.
        const query = { ...this.read(), hb: Date.now() }
        vueRouter.replace({
          query
        })

        // This should be enough, if the race-condition would not be there
        callBack(query)
      }

      window.addEventListener('popstate', this._onPopState)
    },

    /* remove any listeners */
    dispose() {
      if (typeof window === 'undefined') {
        return
      }

      window.removeEventListener('popstate', this._onPopState)
    }

Watcher

watch: {
    '$route.query': {
      handler: async function(newValue, oldValue) {
        if (newValue.hb) {
          const query = { ...newValue }
          delete query.hb
          await this.$router.replace({ query })
          this.state.instantSearchInstance.helper.search()
        }
      }
    }
  }

For now this is the solution we will go with.

All 11 comments

what if the onUpdate is instead implemented like this?

      vueRouter.afterEach(() => {
        cb(this.read());
      });

      if (typeof window === 'object') {
        onPopState = () => {
          if (writeTimer) {
            clearTimeout(writeTimer);
          }

          cb(this.read());
        };
        window.addEventListener('popstate', onPopState);
      }

Hi @Haroenv,

Nothing change with this implementation of onUpdate. :(
The problem exists only I we access to the page directly, not via a NuxtLink.

See with this codesansbox where vue-instantsearch is implemented directly in index.vue

Hi @yoannk-dev have you stumbled on our documentation on the topic and example? You're taking a different approach, it might be worth having a look at them.

Hi @tkrugg,

I just updated the codesandbox with my initial code, which is the mix between this documentation (SSR Nuxt) and this one (Nuxt + Routing).

I'm having the same problem and already created a thread inside the algolia-forum with some additional information:

https://discourse.algolia.com/t/ssr-routing-custom-vue-router-history-back-functionality-uistate-change-not-triggered/12497

@yoannk-dev if you already have a solution, i would be really interest in it.

Hi @elkico,

Thank you for the thread. Unfortunately, I didn't find the solution to fix this problem.

Sounds like the same issue that I had: https://github.com/algolia/vue-instantsearch/issues/916#issuecomment-781390157

@yoannk-dev i have exactly the same issue...
https://github.com/algolia/vue-instantsearch/issues/916#issuecomment-817602414 leads to a infinity route change on my project so it does not help.

i try to navigate to my search page by passing some query params to my nuxt link.. the current nuxt algolia solution seems to ignore that on client side navigation.

this is working for me: https://github.com/algolia/vue-instantsearch/issues/916#issuecomment-827528530

Hi @reinoldus @yoannk-dev and thanks for your feedback.

I tried the solution you posted but it is still not working for me.
I think it is somehow related to the UiState object that gets generated. (if you check the screenshots in my link).

Something i might have said:
Because i'm transforming the UiState to simple route-params, i did not add this to the nuxt.config.js:

// nuxt.config.js
module.exports = {
  router: {
    parseQuery(queryString) {
      return require('qs').parse(queryString);
    },
    stringifyQuery(object) {
      var queryString = require('qs').stringify(object);
      return queryString ? '?' + queryString : '';
    },
  },
};

Because i'm not writing objects into the route-params. Just comma-separated string-values.

We found a solution.

First, the problem is some kind of race-condition, i guess.
The vue-instantsearch callback inside the custom-routers onUpdate (event when doing history back) looks like this:
instantSearchInstance.setUiState(stateMapping.routeToState(route));

Problem is that the UiState got changed but the search is not triggered again.

How i solved it:
Adding a watcher $route.query that triggers the search manually.

Adding this watcher would trigger the search every time the route changes ==> also every time a refinement get's applied and put into the URL.
That would not be bad because vue-instantsearch caches the requests and does not fire them again.

Anyways, to avoid the possibility to trigger request twice, i added a dummy query-parameter when History-Back is used to only trigger the search manually when History-Back was done.

Custom-Router

onUpdate(callBack) {
      if (typeof window === 'undefined') {
        return
      }

      this._onPopState = () => {
        // this is a workaround because of a race-condition: When the UiState get's updated via the callback
        // the search is not triggered again.
        const query = { ...this.read(), hb: Date.now() }
        vueRouter.replace({
          query
        })

        // This should be enough, if the race-condition would not be there
        callBack(query)
      }

      window.addEventListener('popstate', this._onPopState)
    },

    /* remove any listeners */
    dispose() {
      if (typeof window === 'undefined') {
        return
      }

      window.removeEventListener('popstate', this._onPopState)
    }

Watcher

watch: {
    '$route.query': {
      handler: async function(newValue, oldValue) {
        if (newValue.hb) {
          const query = { ...newValue }
          delete query.hb
          await this.$router.replace({ query })
          this.state.instantSearchInstance.helper.search()
        }
      }
    }
  }

For now this is the solution we will go with.

Thank you @elkico for your help and your sharing! Your solution with the watcher works for me too.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fi0 picture fi0  路  3Comments

vasilestefirta picture vasilestefirta  路  4Comments

chamby picture chamby  路  3Comments

shaunnez picture shaunnez  路  3Comments

michaelchiche picture michaelchiche  路  4Comments