algolia / vue-instantsearch

👀 Algolia components for building search UIs with Vue.js
https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/vue
MIT License
853 stars 157 forks source link

Bug with initialUiState. #1065

Closed bootsmann1995 closed 1 year ago

bootsmann1995 commented 3 years ago

Bug 🐞

What is the current behavior?

<ais-instant-search-ssr
                    :initial-ui-state="initialUiState"
                    > 

ais-instant-search-ssr does not support :initial-ui-state feature. I need this on server side since I want to preconfigure my search based on something from my headless cms. And I can't do this in my setup.

I'm using Nuxt Algolia implemented like in your docs:

data() {
        const mixin = createServerRootMixin({
            searchClient: getSearchClient(this.$config ?? process.env),
            indexName: this.$config?.ALGOLIA_INDEX_NAME ?? process.env.ALGOLIA_INDEX_NAME!,
            routing: {
                router: nuxtRouter(this.$router),
            },
            initialUiState: {
                [this.$config?.ALGOLIA_INDEX_NAME]: {
                    refinementList: {
                        business: ["koed"] <--- THIS IS HERE I WANT TO USE MY CMS DATA PROPERTY
                    }
                },
            }
        });
        return {
            ...mixin.data(),
        };
    },
    provide() {
        return {
            // Provide the InstantSearch instance for SSR
            $_ais_ssrInstantSearchInstance: this.instantsearch,
        };
    },
    serverPrefetch() {
        return this.instantsearch?.findResultsState(this).then((algoliaState: any) => {
            this.$ssrContext.nuxt.algoliaState = algoliaState;
        });
    },
    beforeMount() {
        const results =
            (this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
            (window as unknown as IAlgoliaWindow).__NUXT__.algoliaState;

        (this as any).instantsearch.hydrate(results);

        // Remove the SSR state so it can't be applied again by mistake
        delete this.$nuxt.context.nuxtState.algoliaState;
        delete (window as unknown as IAlgoliaWindow).__NUXT__.algoliaState;
    },

I can't use my properties in data() from headless cms so I need some way to set this initialUiState when my cms data is available-

What is the expected behavior?

The expected behavior is I can set my initialUiState programatically. something like this:

data() {
        const cmsProp = this.data.property;

        const mixin = createServerRootMixin({
            searchClient: getSearchClient(this.$config ?? process.env),
            indexName: this.$config?.ALGOLIA_INDEX_NAME ?? process.env.ALGOLIA_INDEX_NAME!,
            routing: {
                router: nuxtRouter(this.$router),
            },
            initialUiState: {
                [this.$config?.ALGOLIA_INDEX_NAME]: {
                    refinementList: {
                        business: [cmsProp ] <--- USED HERE.
                    }
                },
            }
        });
        return {
            ...mixin.data(),
        };
    },

What is the version you are using?

"algoliasearch": "^4.9.3", "vue-instantsearch": "^3.7.0",

tkrugg commented 3 years ago

Hi @bootsmann1995 usually when you want to add state from the server, you'd rely the routing feature, and so you'd be able to pass a state via the url like this: https://github.com/algolia/vue-instantsearch/blob/master/examples/ssr/src/main.js#L44-L45.

Can this work for you? Otherwise, PRs are more than welcome!

bootsmann1995 commented 3 years ago

@tkrugg Thanks for the answer :)

How would you implement it with an object? and isn't this Vue3 ? i'm using nuxt, and it's only in vue2 for now.

bootsmann1995 commented 3 years ago

i'm currenly using this router:

import VueRouter from 'vue-router';
import { Dictionary } from 'vue-router/types/router';

type RouteState = { [key: string]: unknown };

export function nuxtRouter(vueRouter: VueRouter) {
  return {
    read() {
      return vueRouter.currentRoute.query;
    },
    write(routeState: 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 as Dictionary<string | (string | null)[] | null | undefined>,
      });
    },
    createURL(routeState: RouteState) {
      return vueRouter.resolve({
        query: routeState as Dictionary<string | (string | null)[] | null | undefined>,
      }).href;
    },
    onUpdate(cb) {
      if (typeof window === 'undefined') return;

      /* tslint:disable-next-line */
      this._onPopState = (event: IEvent) => {
        const routeState = event.state;
        if (!routeState) {
          cb(this.read());
        } else {
          cb(routeState);
        }
      };
      window.addEventListener('popstate', this._onPopState);
    },
    dispose() {
      if (typeof window === 'undefined') return;

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

it works fine with server navigation. But when routing with nuxt-router it clears the url

bootsmann1995 commented 3 years ago

@tkrugg @Haroenv

bootsmann1995 commented 3 years ago

otherwise is it possible to set the uiState after render? @tkrugg @Haroenv

Haroenv commented 3 years ago

you can set the ui state after render with instantsearchInstance.setUiState, but I still don't get why routing doesn't work for your use case.

Continuing to ping individual people instead of using support@algolia.com however is messing with our processes and as always you haven't given a sandbox for debugging, could you stop doing that please?

bootsmann1995 commented 3 years ago

Haroenv yes ofc. sorry for this. It's just because this issue is under time pressure. I want to set the route based on an object fetched from an api. But if i push my parameter to my router on created it just clears my parameters after load? I sent my algolia routing further up in this thread. It works when routing directly to the page. but not when using vue-router navigation.

Haroenv commented 3 years ago

If you use routing, you can't also use initial UI state, they both act on the same information and routing takes precedence. What you should do is:

  1. add a new property "initial state" or something to the router you created
  2. pass the cms state to the router
  3. merge with the router from the url

Hope that makes sense!

bootsmann1995 commented 3 years ago

Thank you for the answer, do you have an example on this? otherwise i'll try make it work with these steps.

Haroenv commented 3 years ago

something approximately like this: https://codesandbox.io/s/awesome-architecture-shex2?file=/src/App.vue

bootsmann1995 commented 3 years ago

But if I don't have the data from asyncData() in this hook, this sets me back to square one still right?

Haroenv commented 3 years ago

the data in asyncData happens after data if I remember it correctly, without a full example of what you want to do it will be hard to help you out. I recommend simplifying the logic so the state can be derived directly from the page instead of needing a network request

bootsmann1995 commented 3 years ago

Okay do you have a nuxt template. then I'll create a full example for you to clarify :) sorry for the confusion.

But it's because we give the content editor the possibility to pre select a value for the uistate on the cms. and we fetch data from cms in asyncData bc of the static site generation.

Haroenv commented 3 years ago

https://codesandbox.io/s/patient-cdn-393wm?file=/pages/search.vue