Skip to content
This repository has been archived by the owner on Dec 30, 2022. It is now read-only.

Nuxt Instant Search broken when used with router and index mapping #1066

Closed
podlebar opened this issue Sep 30, 2021 · 27 comments
Closed

Nuxt Instant Search broken when used with router and index mapping #1066

podlebar opened this issue Sep 30, 2021 · 27 comments

Comments

@podlebar
Copy link

podlebar commented Sep 30, 2021

Bug 🐞

What is the current behavior?

When used with Nuxt combined with routing and singleIndexMapping the query parameters are removed after hitting refresh.

when this is added on line 106 in the sandbox:

stateMapping: singleIndexMapping("instant_search"),

the route on click on a facet does not change at all.

Make a sandbox with the current behavior

https://codesandbox.io/s/friendly-parm-hy2m2?file=/pages/search.vue

What is the expected behavior?

  1. Clock on Apple in the facets
  2. Url changes to https://hy2m2.sse.codesandbox.io/search?refinement%5BrefinementList%5D%5Bbrand%5D%5B0%5D=Apple
  3. hit the refresh button
  4. the url stays same and the UiState is recreated

Does this happen only in specific situations?

Happens all the time

What is the proposed solution?

What is the version you are using?

algoliasarch 4.10.5
nuxt 2.15.8
vue-instantsearch 4.0.1
vue-server-renderer 2.6.11

@EduardoMateos
Copy link

same here ;(

@podlebar
Copy link
Author

podlebar commented Oct 4, 2021

@EduardoMateos did you find any solution?

@EduardoMateos
Copy link

EduardoMateos commented Oct 4, 2021

Hello,

I fixed it temporarily, it is not the best solution but this works for me. I get the parameters from the url manually to initialize Algolia.

im use Nuxt 2.15.8, IMPORTANT: my index name is "product"

PD:The code can be improved, I did it quickly because I don't have much time. But it works.

Code:

import {
  AisInstantSearchSsr,
  // AisRefinementList,
  AisInfiniteHits,
  AisHighlight,
  AisSearchBox,
  AisStats,
  AisPagination,
  AisSnippet,
  AisStateResults,
  AisPoweredBy,
  createServerRootMixin,
} from "vue-instantsearch";

import algoliasearch from "algoliasearch/lite";
import _renderToString from "vue-server-renderer/basic";
import { createInfiniteHitsSessionStorageCache } from "instantsearch.js/es/lib/infiniteHitsCache";

function renderToString(app) {
  return new Promise((resolve, reject) => {
    _renderToString(app, (err, res) => {
      if (err) reject(err);
      resolve(res);
    });
  });
}

const indexName = "product";

const searchClient = algoliasearch(
  "XXXXX",
  "XXXXX"
);

// read and write router state to support algolia query parameter
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) {
      const url = vueRouter.resolve({
        query: routeState,
      }).href;
      return url;
    },
    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 {
  serverPrefetch() {
    return this.instantsearch
      .findResultsState({
        component: this,
        renderToString,
      })
      .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;
  },
  data() {
    //FIX SSR - GET PARAMETERS FROM URL TO INITIALIZE ALGOLIA
    var initialUiState = {};
    initialUiState.product = {};
    initialUiState.product.refinementList = {};

    //QUERY search
    if (
      this.$router.currentRoute.query.product &&
      this.$router.currentRoute.query.product.query
    ) {
      initialUiState.product.query =
        this.$router.currentRoute.query.product.query;
    }

    //filters
    if (
      this.$router.currentRoute.query.product &&
      this.$router.currentRoute.query.product.refinementList &&
      this.$router.currentRoute.query.product.refinementList.attr_sexo
    ) {
      initialUiState.product.refinementList.attr_sexo =
        this.$router.currentRoute.query.product.refinementList.attr_sexo;
    }

    if (
      this.$router.currentRoute.query.product &&
      this.$router.currentRoute.query.product.refinementList &&
      this.$router.currentRoute.query.product.refinementList.attr_marca
    ) {
      initialUiState.product.refinementList.attr_marca =
        this.$router.currentRoute.query.product.refinementList.attr_marca;
    }

    if (
      this.$router.currentRoute.query.product &&
      this.$router.currentRoute.query.product.refinementList &&
      this.$router.currentRoute.query.product.refinementList.attr_silo
    ) {
      initialUiState.product.refinementList.attr_silo =
        this.$router.currentRoute.query.product.refinementList.attr_silo;
    }



    const mixin = createServerRootMixin({
      searchClient,
      indexName,
      routing: {
        router: nuxtRouter(this.$router),
      },
      initialUiState: initialUiState,
    });
    return {
      ...mixin.data(),
      cache: createInfiniteHitsSessionStorageCache(),
    };
  },
  provide() {
    return {
      $_ais_ssrInstantSearchInstance: this.instantsearch,
    };
  },
  components: {
    AisInstantSearchSsr,
    AisInfiniteHits,
    AisHighlight,
    AisSearchBox,
    AisStats,
    AisPagination,
    AisSnippet,
    AisStateResults,
    AisPoweredBy,
  },
};

@Haroenv
Copy link
Contributor

Haroenv commented Oct 4, 2021

What I'm seeing so far is that the write function gets called with an empty state when the widgets get added, instead of waiting until everything is finished (although the defer should enforce that already).

This was introduced in version 4.29.1 of InstantSearch.js, so forcing the dependency of InstantSearch.js to 4.29.0 will avoid this issue (algolia/instantsearch#4849 is what causes it exactly, still trying to figure out how though). You can do that by using resolutions in package.json if you're using Yarn:

{
  "resolutions": { "instantsearch.js": "4.29.0" }
}

thanks for reporting!

@podlebar
Copy link
Author

podlebar commented Oct 5, 2021

@Haroenv .. nah that does not fix it for me.. instead i get now:

"export 'connectDynamicWidgets' (imported as 'e') was not found in 'instantsearch.js/es/connectors'                                friendly-errors 08:44:42

@Haroenv
Copy link
Contributor

Haroenv commented Oct 5, 2021

That's just a warning @podlebar, unless you're using dynamic widgets? (then you'll need to alias connectDynamicWidgets to EXPERIMENTAL_connectDynamicWidgets`

@podlebar
Copy link
Author

podlebar commented Oct 5, 2021

yes true.. sorry for that. but the routing behavior is still the same. the route gets overwritten to a empty one.. but i feel like it's happening later than before.

but in my updated sandbox it seems to work fine now..

and no i don't use the dynamic widget at all.

@podlebar
Copy link
Author

podlebar commented Oct 5, 2021

i just tried a hack to now write any rout in the router() function unless the initial query wasn't empty and the user has not interacted with the widgets.. this works fine.. the url keeps staying in the url bar but the serchresults are the initial ones.. so there might be a issue with the indexMapping perhaps.

@Haroenv
Copy link
Contributor

Haroenv commented Oct 5, 2021

I'm guessing the resolutions didn't actually get applied if you're describing that behaviour @podlebar

@podlebar
Copy link
Author

podlebar commented Oct 5, 2021

well.. i checked my node_modules folder and it was set correctly to 4.29.0.

i will do some more testing tomorrow

@podlebar
Copy link
Author

podlebar commented Oct 7, 2021

@Haroenv seems that "instantsearch.js": "4.19.0" is the last version that reads the url correctly but still writes a empty one after load.. but at least it keeps the results and there is no full reset.

@Haroenv
Copy link
Contributor

Haroenv commented Oct 7, 2021

hmm, are you sure you need to go that low? 4.20.0 doesn't introduce any changes related to routing @podlebar. With the full reset, what do you mean exactly? is it possible you have array/function props in the template itself instead of in data?

@podlebar
Copy link
Author

podlebar commented Oct 7, 2021

well with "full reset" i mean that i set a filter.. url changes.. results change. Than i reload and the results are correct but the url gets cleared.

i'm back on 4.29.0 now as the other version did not have the correct renderState on initial load.

@podlebar
Copy link
Author

podlebar commented Oct 8, 2021

@EduardoMateos there is a much simpler way:

updated() {
    if (!this.updatedOnce) {
      this.updatedOnce = true
      setTimeout(
        () => this.instantsearch.setUiState(this.instantsearch._initialUiState),
        300
      )
    }
  },

if you lock the instantsearch.js dep to 4.29.0 like @Haroenv mentioned.. it works really smooth. the 300 milliseconds are somehow a magic number :)

@EduardoMateos
Copy link

@EduardoMateos hay una forma mucho más sencilla:

updated() {
    if (!this.updatedOnce) {
      this.updatedOnce = true
      setTimeout(
        () => this.instantsearch.setUiState(this.instantsearch._initialUiState),
        300
      )
    }
  },

si bloquea instantsearch.js dep en 4.29.0 como mencionó @Haroenv ... funciona realmente sin problemas. los 300 milisegundos son de alguna manera un número mágico :)

Thank you!!! I will apply your solution.

@sarahdayan
Copy link
Member

We've filed this internally and will schedule a fix, thanks for reporting! We'll follow up here when we tackle it.

@podlebar
Copy link
Author

podlebar commented Nov 1, 2021

@sarahdayan any update on this or sort of a timeline?

@sarahdayan
Copy link
Member

This isn't shcheduled yet, but we'll let you know here as soon as it is.

Haroenv added a commit to algolia/instantsearch that referenced this issue Jan 27, 2022
fixes algolia/vue-instantsearch#1066
fixes algolia/vue-instantsearch#1062

see: #4849

In Vue InstantSearch SSR, the mainIndex gets init'ed *before* it does in regular InstantSearch (see https://codesandbox.io/s/beautiful-mestorf-ijc75?file), and at that point the ui state should already be initialized (before start).
@Haroenv
Copy link
Contributor

Haroenv commented Jan 31, 2022

We have just finished a comprehensive refresh of the server-side interaction with routing in the case of Vue InstantSearch. This managed to close this issue for both v3 in 3.9.0 and v4 in 4.3.2.

The pull requests in question are:

If the component at the root is the one doing findResultsState, it doesn't have access to $vnode, and in that branch the component name is required to be passed via the first argument.

This is a little more involved, but fixes the issue where routing is not taken in account. Essentially in InstantSearch it was changed so that routing only gets read on .start, but Vue InstantSearch was faking start.

Luckily, in a recent version of InstantSearch, some functionality of server rendering / initial results has moved inside InstantSearch itself, and it avoids the issue, as we can use regular 'start' from then on.

@Haroenv Haroenv closed this as completed Jan 31, 2022
@podlebar
Copy link
Author

podlebar commented Feb 2, 2022

@Haroenv Hi.. and first off all thank you for take care of the issue.
So can i now safely update all the packages to the newest versions when using vue 2 and nuxt 2?

@podlebar
Copy link
Author

podlebar commented Feb 2, 2022

i just tried it out and it is still not fixed.. event my workarounds stop working as it seems that the method setUiState was removed

@podlebar
Copy link
Author

podlebar commented Feb 2, 2022

short Update: yes the url gets now read on SSR and the results are correct.. but the UI is wrong.. it does not reflect the url

@Haroenv
Copy link
Contributor

Haroenv commented Feb 2, 2022

The setUiState method wasn't removed, could you explain what you mean @podlebar? I'd love to follow up to find what's causing any wrong behaviour!

@podlebar
Copy link
Author

podlebar commented Feb 2, 2022

hard to say what causes this behavior..
i still need a methods like this:

  updated() {
    if (!this.updatedOnce) {
      this.updatedOnce = true
      if (
        this.instantsearch._initialUiState[process.env.algolia.productIndex] &&
        this.instantsearch._initialUiState[process.env.algolia.productIndex]
          .refinementList
      ) {
        setTimeout(() => {
          this.instantsearch.setUiState(this.instantsearch._initialUiState)
        }, 200)
      }
    }
  },

to make the url map with the UI state.

You are right.. the method still exists but somehow i could not track it down anymore.

@Haroenv
Copy link
Contributor

Haroenv commented Feb 2, 2022

@podlebar, if you could create a repo that shows this problem clearly, I'll look into it today. In testing this PR (and in the tests), a refinementList is used, as well as multi-index, so in theory what you need that patch for is already handled. Thanks for your cooperation!

@podlebar
Copy link
Author

podlebar commented Feb 2, 2022

hey.. will try to do one and track the problems down to be more specific.. but i guess it won't be today

@Haroenv
Copy link
Contributor

Haroenv commented Feb 2, 2022

Thanks @podlebar, please open a new issue with it, as it's likely a different thing going on than what caused this issue initially

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants