vue-instantsearch icon indicating copy to clipboard operation
vue-instantsearch copied to clipboard

Nuxt + SSR + Routing - Pushes route twice

Open reinoldus opened this issue 4 years ago • 10 comments

Bug 🐞

What is the current behavior?

I am trying to implement the nuxt + ssr + routing examples from the docs: https://www.algolia.com/doc/guides/building-search-ui/going-further/routing-urls/vue/#combining-with-vue-router

Issue 1: Route is pushed twice so you have to click back twice to get back

I recoreded a gif so you can reproduce the steps yourself: Peek 2021-02-18 09-05

This sandbox (serene-leftpad-jp7gb) contains basically just code from the docs https://codesandbox.io/s/serene-leftpad-jp7gb

What is the expected behavior?

Route only pushed once

Does this happen only in specific situations?

No

What is the proposed solution?

I don't know

What is the version you are using?

3.2.0

Always try the latest one before opening an issue.

reinoldus avatar Feb 18 '21 07:02 reinoldus

Quick fix for this problem, just check if the query truly changed. Not pretty but gets the job done.

function nuxtRouter(vueRouter) {
  return {
    read() {
[...]
    },
    write(routeState) {
      if (this.createURL(routeState) !== this.createURL(this.read())) {
        vueRouter.push({
          query: {
            ...routeState,
          },
        })
      }
    },
    createURL(routeState) {
      return vueRouter.resolve({
        query: routeState,
      }).href
    },
[...]
  }
}

export { nuxtRouter }

Additionally to that I have another issue which I can't replicate in a sandbox but it is real. If someone has an idea what could go wrong here please let me know:

Scenario:

  1. User visits: example.com/search?brand=ABC
  2. The brand ABC is selected accordingly through SSR - all good so far
  3. User selects additional refinement filters e.g. category=dog
  4. Clicks on a product
  5. Goes back to search via browser back button

What is expected?

State: brand=ABC&category=dog

What happens?

The original SSR state is shown meaning: brand=ABC

No matter what I select (even unselecting everything) the original SSR state is shown.

If I visit the search page through example.com/ -> [navigate to] -> example.com/search. Everything works as expected.

After 2 days of debugging I found this "solution" -> nuke the algoliaState after it was used in beforeMount:

this.$nuxt.context.nuxtState.algoliaState = undefined
window.__NUXT__.algoliaState = undefined

reinoldus avatar Feb 18 '21 14:02 reinoldus

thanks for passing this workaround, it's indeed correct, and so is the workaround to your second problem. I'll update the documentation to include these points!

Haroenv avatar Feb 18 '21 14:02 Haroenv

Heya, had to test this to see if it was a nuxt problem or a vue storefront problem: made a minimal repo: https://github.com/ed42311/algolia-nuxt-routes

To clarify: it seems that that the problem I'm seeing is with VSF, but I thought this might be useful

ed42311 avatar Apr 09 '21 22:04 ed42311

The documentation has not been updated yet, but a fully correct router that works with back button in vue should be:

function nuxtRouter(vueRouter) {
  let writeTimer = null;
  let removeAfterEachListener = null;
  let popStateListener = null;

  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;
      }

      if (writeTimer) clearTimeout(writeTimer);

      writeTimer = setTimeout(
        () =>
          vueRouter.push({
            query: routeState,
          }),
        400
      );
    },
    createURL(routeState) {
      return vueRouter.resolve({
        query: routeState,
      }).href;
    },
    onUpdate(cb) {
      removeAfterEachListener = vueRouter.afterEach((to, from) => {
        console.log('haaah', to.query)
        cb(to.query);
      });

      if (typeof window === "undefined") return;

      popStateListener = () => {
        cb(this.read());
      };
      window.addEventListener("popstate", this._onPopState);
    },
    dispose() {
      if (writeTimer) clearTimeout(writeTimer);

      if (removeAfterEachListener) removeAfterEachListener();

      if (typeof window === "undefined") return;

      window.removeEventListener("popstate", popStateListener);
    },
  };
}

Haroenv avatar Apr 12 '21 08:04 Haroenv

@Haroenv this solution lead to a infinit route change on my project.. so every 400ms i see the url without any params that it shows them and removes them again..

this code seems to work for my project at least:

import qs from 'qs'

function nuxtRouter(vueRouter) {
  const routeWritten = null
  let popStateListener = null

  return {
    read() {
      return qs.parse(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
      }

      if (!routeWritten) {
        setTimeout(
          () =>
            vueRouter.push({
              query: routeState
            }),
          400
        )
      }
    },
    createURL(routeState) {
      return vueRouter.resolve({
        query: routeState
      }).href
    },
    onUpdate(cb) {
      if (typeof window === 'undefined') return

      popStateListener = () => {
        cb(this.read())
      }
      window.addEventListener('popstate', this._onPopState)
    },
    dispose() {
      if (typeof window === 'undefined') return

      window.removeEventListener('popstate', popStateListener)
    }
  }
}

podlebar avatar Apr 27 '21 11:04 podlebar

just found a small bug..

read() {
  return vueRouter.currentRoute.query
},

is not eqal:

read() {
  return qs.parse(vueRouter.currentRoute.query)
},

so if i use qs.parse in read() it also works on client side navigation with query params.

podlebar avatar Apr 27 '21 11:04 podlebar

using qs is best done in the vue router configuration itself, and not in the router here: https://www.algolia.com/doc/guides/building-search-ui/going-further/routing-urls/vue/#combining-with-vue-router

Haroenv avatar Apr 27 '21 11:04 Haroenv

yes.. but in my case it was the only way i could fix it as the result of read() was on SSR a different one as on the client side. but as suggested i also use it in my nuxt config as described.

podlebar avatar Apr 27 '21 12:04 podlebar

found this awhile back: https://code.luasoftware.com/tutorials/algolia/setup-algolia-instantsearch-on-nuxt-with-query-url/

seems to resolve the loading error, probably because of the callback timing?

also lets you clean up the url

ed42311 avatar Apr 27 '21 15:04 ed42311

something else i discoverd this morning: if you keep some views alive in nuxt the solution is not working. You need to exclude the component than includes the Algolia Components from the keep-alive prop.

podlebar avatar Apr 28 '21 07:04 podlebar

I'll close this now, as likely most context is no longer valid. The InstantSearch.js history router has since added some code to ensure you don't write multiple times to the same url. If you have a custom router, I recommend taking inspiration from there and checking before writing

Haroenv avatar Dec 21 '22 15:12 Haroenv