Nuxt doesn't render the ais components SSR

Hello!

I’ve followed the example for configuring Nuxt SSR. It renders perfectly client-side. Unfortunatly, when I go and see page source, I see <ais-search-box></ais-search-box> <ais-menu>…etc but not the actual content. As long as I’ve understood, I should be able to see the html results as a string, and an object containing the results, which is used later for hydration.

Furthermore, because of that, I have the following warning:

[InstantSearch.js]: The UI state for the index “products” is not consistent with the widgets mounted.

Which of course is normal, because nothing is rendered.

Here is my code:

<template>
  <ais-instant-search-ssr>
    <slot />
  </ais-instant-search-ssr>
</template>

export default {
    mixins: [
      createServerRootMixin({
        searchClient,
        indexName: 'products',
      }),
    ],
serverPrefetch() {
  return this.instantsearch.findResultsState(this).then(algoliaState => {
    this.$ssrContext.nuxt.algoliaState = algoliaState
  })
},
beforeMount() {
  const results =
    this.$nuxt.context.nuxtState.algoliaState ||
    window.__NUXT__.algoliaState

  this.instantsearch.hydrate(results)
},
}

Please help.

One thing I can notice is that there’s a slot with the content inside. This might cause the prefetch to happen before the slot’s contents are initialised, or possibly the slot doesn’t work for getting the data.

To confirm this is what’s going on, could you make your application without the slot and see if it works in that case?

For information, which version of nuxt & vue are you using?

1 Like

I’m also wondering how you’re using this component, whether it’s just a component or a layout or something different

1 Like

Hello. Thanks for your reply!

I’m using Nuxt 2.14.5 and Vue 2.6.12.

One thing that’s probably important to say is that this component is a wrapper for the whole page (it’s in the default layout, called InstantSearch). The reason for that is because I have search input at the top of the page, which is reachable from everywhere in the application. The search results are visible only in the search page.

<template>
    <InstantSearch>
       <Topbar />
       <SiteHeader />
       <b-container class="my-3" fluid>
           <slot />
       </b-container>
       <SiteFooter />
       <CookiePolicySticky />
    </InstantSearch>
</template>
1 Like

SiteHeader.vue

<template>
<header>
<b-container class="d-flex" fluid>
 ...
  <nav class="nav nav-main nav-gap-x-1 nav-pills ml-3 d-none d-lg-flex">
    <SearchInput />
  </nav>
...
  </b-container>
 </header>
</template>

SearchInput.vue

<template>
<ais-search-box>
 <template #default="{ currentRefinement, refine }">
  <div class="input-group border-bottom">
    <input
      ref="search-input"
      class="form-control shadow-none border-0 px-lg-2"
      type="search"
      style="width: 300px; background-color: transparent"
      placeholder="Намерете си нещо хубавичко..."
      autocomplete="off"
      spellcheck="false"
      @input="input($event.target.value, refine)"
      @keypress.enter="search(refine)"
      :value="currentRefinement"
    />
    <div class="input-group-append">
      <button
        class="btn btn-text-secondary btn-icon rounded-circle"
        @click="search(refine)"
      >
        <Icon name="search" />
      </button>
    </div>
  </div>
</template>
 <script>
  export default {
    data: () => ({
      query: '',
    }),
    methods: {
      search(refine) {
        refine(this.query)
        this.$router.push('/tyrsene')
      },
      input(value, refine) {
        this.query = value
        if (value == '') {
          this.search(refine)
        }
      },
      focusInput() {
        setTimeout(() => {
          this.$refs['search-input'].focus()
        }, 1000)
      },
    },
    mounted() {
      this.focusInput()
    },
   }
  </script>

I hope this helps clarifying things.

1 Like

I’ve tried like this:

<template>
  <ais-instant-search-ssr>
    <ais-search-box />
  </ais-instant-search-ssr>
</template>

But the result is the same.

have you imported the components and used them in components: { AisInstantSearchSsr, AisSearchBox } here?

I confirm that slots are problematic and don’t work yet at this point at the root level, still looking for a soution

1 Like

I haven’t because I have turned on the autoimport feature of Nuxt. After importing them, it renders everything correctly server side. I wasn’t expecting to be required to import the components since I have the autoimport feature on.

Thank you very much!

Now I’ve added routing, and because of the slug I’ve added between the ais-instant-search-ssr tag, I have a warning saying that I don’t have a corresponding ais-search-box for the ‘query’ param. When I added <ais-search-box> instead of the slot, it doesn’t show this warn.

What is the correct way of implementing such kind of scenario? Maybe I am not doing it right. Should I wrap every ais-* component with ais-instant-search-ssr? As long as I understood, I must not because I will create many instant search instances, while the goal is to create one instance, which is controlling all other widgets/components. That’s why I’ve implemented it that way.

Thank you very much again!

1 Like

Yes, only one instance of InstantSearch is possible to use. If you’ve got a full application to look at, that will help a lot, since I’m not sure what it’s laid out as that would require multiple roots

1 Like

I’ve created a github clone. I will remove it after you have a look at it. The changes related to the topic have message “WIP”. Thank you!

1 Like

The page still requires quite a lot of setup, so I’m not yet fully sure what’s all going on, sorry. In which pages is InstantSearch used?

1 Like

It’s only used in the DefaultLayout compoenent. Should I wrap all ais components with it? I don’t know which is the correct way and why.

Ok, I’ve experimented a lot, and it looks like ais-instant-search and ais-instant-search-ssr must have other ais components as direct children. Which means if I want to create reusable components for example for showing results (ais-hits wrapper) or for pagination (ais-paginate wrapper), I have to create routing/state mapping for each of them, besides just wrapping them around ais-instant-search-ssr component. I need reusable components, because I use them on three places in the application, and maybe they will be more in the future.

This is not very obvious in the documentation, and I was confused. Basically what I was thinking is that when I pass createServerRootMixin, it creates an instant search instance, which is actually the Helper, which makes the queries. Is that correct? Then, we pass that instance to the other components via provide. So, because of that, the nesting should not matter. It appears it does. That’s why was the confusion. It’s a bit hard to understand what is going on without looking at the source code. At the end, I came out with this mixin:

    import algoliasearch from 'algoliasearch/lite'
    import { createServerRootMixin, AisInstantSearchSsr } from 'vue-instantsearch'

    const searchClient = algoliasearch(
      process.env.NUXT_ENV_ALGOLIA_SEARCH_APP_ID,
      process.env.NUXT_ENV_ALGOLIA_SEARCH_API_KEY,
    )

function nuxtRouter(vueRouter) {
  return {
    read() {
      return vueRouter ? vueRouter.currentRoute.query : ''
    },
    write(routeState) {
      if (!vueRouter) 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
        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 function(stateMapping) {
  return {
    components: {
      AisInstantSearchSsr,
    },
    provide() {
      return {
        $_ais_ssrInstantSearchInstance: this.instantsearch,
      }
    },
    data() {
      const mixin = createServerRootMixin({
        searchClient,
        indexName: 'products',
        routing: {
          router: nuxtRouter(this.$router),
          stateMapping,
        },
      })

      return {
        ...mixin.data(),
      }
    },
    serverPrefetch() {
      return this.instantsearch.findResultsState(this).then(algoliaState => {
        this.$ssrContext.nuxt.algoliaState = algoliaState
      })
    },
    beforeMount() {
      const results =
        this.$nuxt.context.nuxtState.algoliaState ||
        window.__NUXT__.algoliaState

      this.instantsearch.hydrate(results)
    },
  }
}

It appears that in situations where the ais components are on different places on the page, and not together like in the examples, it’s not very obvious how to do the things right.

Actually I realize that the children of ais-instant-search must be direct because otherwise they won’t be rendered until their parents are rendered. That’s why is the warn: “In order to have ‘query’ you must have searchbox, or infinitebox widgets”. Is that right?

In the mean time I’ve found the source of the slots not working. If you could try this fix out in your node modules, that would be really nice. You can use patch-package to add this line to es/src/util/createServerRootMixin, where it will be r.$slots = e.$slots; at the end of the first promise.

see the PR: https://github.com/algolia/vue-instantsearch/pull/898

I’m sorry I couldn’t understand where exactly. Could you please tell the line: https://github.com/algolia/vue-instantsearch/blob/master/src/util/createServerRootMixin.js

these lines: https://github.com/algolia/vue-instantsearch/blob/9f59e857eec295ba82023754817ee76e42295b1f/src/util/createServerRootMixin.js#L91-L92

in the compiled version it looks like this:

((r = new i({
  propsData: e.$options.propsData,
})).$options.serverPrefetch = []),
  (r.instantsearch.helper = d),
  (r.instantsearch.mainHelper = d),
  (r.$slots = e.$slots),
  r.instantsearch.mainIndex.init({
    instantSearchInstance: r.instantsearch,
    parent: null,
    uiState: r.instantsearch._initialUiState,
  });
1 Like

It’s rendering properly but I have this warn:

WARN [InstantSearch.js]: The UI state for the index “products” is not consistent with the widgets mounted.

  • query needs one of these widgets: “searchBox”, “autocomplete”, “voiceSearch”

I’ve set stateMapping in the InstantSearch component, and ais-search-box is a direct child of InstantSearch. This time AisSearchBox is added in the component’s section.

1 Like

Hmm, if it’s rendering correctly, that warning shouldn’t show :thinking: Can you reproduce in a simple sandbox with just the slots?

1 Like

This is how I’ve implemented it on my side: https://codesandbox.io/s/competent-darkness-93vc4?file=/components/InstantSearch.vue

Unfortunately, it gives Bad Gateway error, which also happens in the original sandbox from the examples: https://codesandbox.io/s/github/algolia/vue-instantsearch/tree/master/examples/nuxt, but you’ll get the idea.

If ais-search-box is a direct child of ais-instant-search-ssr, there is no problem. Like this:

<ais-instant-search-ssr>
    <ais-search-box />
</ais-instant-search-ssr>

The examples in the official documentation (the showcase) represent an ideal scenario in which all components are on one page, and are all nested under the main ais-instant-search component. It would be great to create an example in the case where things are more close to the real world. For example, if the search is in the header but the results are on another page. Sometimes I’ve even see the search input in the footer as well. This is interesting case because routing is also involved. You have to redirect the user, and at the same time to chang the query parameter in the url, which reflects the state of the ais components.

I could help but now I’m not so sure, which is the best way to do it. If I am, I’d create a sandbox.

1 Like

I’ve now released v3.4.3 of Vue InstantSearch, which allows you to put the fetching logic & InstantSearch component at Nuxt layouts or in a component with slots :slight_smile: