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

AisInstantSearchSsr does not work well with nuxt i18n translation functions #936

Closed
podlebar opened this issue Mar 23, 2021 · 26 comments
Closed

Comments

@podlebar
Copy link

podlebar commented Mar 23, 2021

Bug 🐞

What is the current behavior?

I use nuxt.js with and for the translation of the whole website i use the official nuxt i18n module (https://i18n.nuxtjs.org/).
The common function to translate strings with nuxt i18n is is $t(). When i place this somewhere inside the component i get the error "TypeError: Cannot read property '_t' of undefined". Also i use the component with transform-items where i translate the labels into the desired language but this leads to the same error.

Make a sandbox with the current behavior

Not possible as true SSR with Nuxt is needed..

What is the expected behavior?

The $t() function should be usable inside the component without any ugly workaround.
My workaround is currently to re-render all components as soo the mounted()hook is called.

Does this happen only in specific situations?

No.. in al situations where i use a translation function inside a component.

What is the proposed solution?

Make $t() available in or find a way to inject $t() and $tc() functions into . ($tc() is used for pluralization.)

What is the version you are using?

"nuxt": "^2.14.3"
"vue-instantsearch": "^3.5.0",
"algoliasearch": "^4.8.6",

@Haroenv
Copy link
Contributor

Haroenv commented Mar 24, 2021

I can't find the source code easily of $t, but it might be https://github.com/nuxt-community/i18n-module/blob/master/src/index.js#L105 or related. Basically the code in Vue InstantSearch related to this is:

const options = {
serverPrefetch: undefined,
fetch: undefined,
_base: undefined,
name: 'ais-ssr-root-component',
// copy over global Vue APIs
router: componentInstance.$router,
store: componentInstance.$store,
};
const Extended = componentInstance.$vnode
? componentInstance.$vnode.componentOptions.Ctor.extend(options)
: Vue.component(
Object.assign({}, componentInstance.$options, options)
);
app = new Extended({
propsData: componentInstance.$options.propsData,
});
// https://stackoverflow.com/a/48195006/3185307
app.$slots = componentInstance.$slots;
app.$root = componentInstance.$root;
app.$options.serverPrefetch = [];

It makes a copy of the current component to render intermediary, to get access to the widgets that are its children in serverPrefetch. We should open that up to be overridable so you can retrieve the $t function yourself without us necessarily having to make a patch in the library.

Similar issues we've fixed before: #898 #865 #864 #863

Haroenv added a commit that referenced this issue Mar 24, 2021
related to #936, this would allow people to link items to the cloned component themselves
@Haroenv
Copy link
Contributor

Haroenv commented Mar 24, 2021

in #937 I'm adding a way to override this manually for the time being (once we release this). In the mean time, it would be really interesting if you could experiment to see what needs to be added to the cloned component to work with nuxt i18n.

You can try out this PR by following these instructions: https://ci.codesandbox.io/status/algolia/vue-instantsearch/pr/937/builds/112665

@podlebar
Copy link
Author

Possible that this PR changes something in the behavior of ais-toggle-refinement ? when i install it i get hasRatings is not defined in the disjunctiveFacets attribute of the helper configuration while hasRatings is a ais-toggle-refinement component.

@Haroenv
Copy link
Contributor

Haroenv commented Mar 25, 2021

Hmm, that PR doesn't actually change anything, it only gives you the possibility to override how the root component is cloned. Do you have a sandbox? I feel like that should behave like this regardless of the change

@Haroenv
Copy link
Contributor

Haroenv commented Mar 25, 2021

Here's how you can use the version from the PR: https://codesandbox.io/s/nowd3 (with the fix for i18n added, which will come after)

function $cloneComponent(componentInstance) {
  const options = {
    serverPrefetch: undefined,
    fetch: undefined,
    _base: undefined,
    name: 'ais-ssr-root-component',
    // copy over global Vue APIs
    router: componentInstance.$router,
    store: componentInstance.$store,

    // added this
    i18n: componentInstance.$i18n,
  };

  const Extended = componentInstance.$vnode.componentOptions.Ctor.extend(
    options
  );

  const app = new Extended({
    propsData: componentInstance.$options.propsData,
  });

  // https://stackoverflow.com/a/48195006/3185307
  app.$slots = componentInstance.$slots;
  app.$root = componentInstance.$root;
  app.$options.serverPrefetch = [];

  return app;
}

export default {
    provide() {
      return {
        $_ais_ssrInstantSearchInstance: this.instantsearch,
      };
    },
  data() {
    // Create it in `data` to access the Vue Router
    const mixin = createServerRootMixin({
      searchClient,
      indexName: 'instant_search',
      $cloneComponent,
    });
    return {
      ...mixin.data(),
    };
  },
// ...

@podlebar
Copy link
Author

i can confirm that it works fine with that version. There are no SSR errors when using any translation function inside the ais-instant-search-ssr component.

Thank you very much.
Is it save to deploy my project to production like that? i guess the fix will be released in some future release.

@Haroenv
Copy link
Contributor

Haroenv commented Mar 26, 2021

Yes, it's safe to use in production, just check whenever you update that we didn't change the name of the parameter or something!

Haroenv added a commit that referenced this issue Mar 31, 2021
related to #936, this would allow people to link items to the cloned component themselves
@eunjae-lee
Copy link
Contributor

Hey @podlebar , #937 has been released in 3.6.0.

createServerRootMixin({
  $cloneComponent(componentInstance) => { ... }
})

Let us know how it goes cc @Haroenv

@podlebar
Copy link
Author

Hi.. i updated and it works just fine..
Thank you very much for your efforts @Haroenv and @eunjae-lee.

@eunjae-lee
Copy link
Contributor

Thanks, glad to hear that!

@adamchipperfield
Copy link

Not sure if I'm missing something but I'm not clear how I can sovle this issue. Is there a version of the vue-instantsearch package I need to change to?

@podlebar
Copy link
Author

podlebar commented Nov 2, 2021

@adamchipperfield no...

add this function to the page:

function $cloneComponent(componentInstance, { mixins = [] } = {}) {
  const options = {
    serverPrefetch: undefined,
    fetch: undefined,
    _base: undefined,
    name: 'ais-ssr-root-component',
    i18n: componentInstance.$i18n,
    router: componentInstance.$router,
    store: componentInstance.$store
  }

  const Extended = componentInstance.$vnode
    ? componentInstance.$vnode.componentOptions.Ctor.extend(options)
    : Vue.component(
        options.name,
        Object.assign({}, componentInstance.$options, options)
      )

  const app = new Extended({
    propsData: componentInstance.$options.propsData,
    mixins: [...mixins]
  })

  app.$slots = componentInstance.$slots
  app.$root = componentInstance.$root
  app.$options.serverPrefetch = []

  return app
}

the important part here is this: i18n: componentInstance.$i18n,.

in you data() function where you created your mixin add this:

const mixin = createServerRootMixin({
      searchClient,
      $cloneComponent,
      indexName: 'YOUR INDEX',
      routing: {
        router: nuxtRouter(this.$router),
        stateMapping: singleIndexMapping('YOUR INDEX')
      }
    })

the important part here is: $cloneComponent...

but currently the library is quit broken in combination with Nuxt. There are a lot workarounds needed to make it really SSR.

@adamchipperfield
Copy link

Thanks @podlebar. Where are nuxtRouter and singleIndexMapping coming from?

@adamchipperfield
Copy link

Tried without the routing object for now and I get "createServerRootMixin is required when using SSR." Any ideas? Clearly using it as above:

image

@Haroenv
Copy link
Contributor

Haroenv commented Nov 3, 2021

you're not doing the "provide" part of the mixin @adamchipperfield

    provide() {
      return {
        $_ais_ssrInstantSearchInstance: this.instantsearch,
      };
    },

@adamchipperfield
Copy link

Thanks, that solved the SSR issue! Weirdly though when you visit the page directly (not using the router) then none of the Algolia components work (i.e. the refineNext method isn't working + sorting using ais-sort-by).

This is since adding the above SSR support.

@adamchipperfield
Copy link

Along with this console error...
image

@podlebar
Copy link
Author

podlebar commented Nov 3, 2021

maybe post your code.. makes it easier

singleIndexMapping needs to be imported:
import { singleIndex as singleIndexMapping } from 'instantsearch.js/es/lib/stateMappings'

@adamchipperfield
Copy link

This is my bad! I had some other errors which came up due to mismatching nodes.

It does seem to work without the singleIndexMapping (or even the whole routing object you originally mentioned). Is this providing something essential?

@podlebar
Copy link
Author

podlebar commented Nov 3, 2021

well yes.. if you want the url to change when you click on a facet you need this.. because than you can reload the browser and the url will provide the state for the search.. otherwise your facets will belost after a reload

@adamchipperfield
Copy link

@podlebar Where am I importing nuxtRouter from here? I'm referring to this comment on the routing object.

Also just on the above about singleIndexMapping, this obviously would mean I'd need to install instantsearch.js on the project too right? Feels like a lot to install for just one thing but correct me if I'm being dumb here. Happy to just do it if it gets everything working.

@podlebar
Copy link
Author

@adamchipperfield

<script>
import {
  AisInstantSearchSsr,
  AisConfigure,
  AisRefinementList,
  AisCurrentRefinements,
  AisInfiniteHits,
  AisRangeInput,
  AisSearchBox,
  AisStats,
  AisClearRefinements,
  AisToggleRefinement,
  createServerRootMixin
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import { singleIndex as singleIndexMapping } from 'instantsearch.js/es/lib/stateMappings'
import _renderToString from 'vue-server-renderer/basic'


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

function nuxtRouter(vueRouter) {
  return {
    read() {
      return vueRouter.currentRoute.query
    },
    write(routeState) {
      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
        if (!routeState) {
          cb(this.read())
        } else {
          cb(routeState)
        }
      }
      window.addEventListener('popstate', this._onPopState)
    },
    dispose() {
      if (typeof window === 'undefined') return

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

function $cloneComponent(componentInstance, { mixins = [] } = {}) {
  const options = {
    serverPrefetch: undefined,
    fetch: undefined,
    _base: undefined,
    name: 'ais-ssr-root-component',
    i18n: componentInstance.$i18n,
    router: componentInstance.$router,
    store: componentInstance.$store
  }

  const Extended = componentInstance.$vnode
    ? componentInstance.$vnode.componentOptions.Ctor.extend(options)
    : Vue.component(
        options.name,
        Object.assign({}, componentInstance.$options, options)
      )

  const app = new Extended({
    propsData: componentInstance.$options.propsData,
    mixins: [...mixins]
  })

  app.$slots = componentInstance.$slots
  app.$root = componentInstance.$root
  app.$options.serverPrefetch = []

  return app
}

export default {
  name: 'WineFinder',
  components: {
    VueSlider,
    ProductTeaser,
    AisInstantSearchSsr,
    AisConfigure,
    AisRefinementList,
    AisRangeInput,
    AisCurrentRefinements,
    AisInfiniteHits,
    AisSearchBox,
    AisClearRefinements,
    AisToggleRefinement,
    AisStats,
    DropdownIcon: () =>
      import(/* webpackChunkName: "icons" */ '~/assets/svg/ui/dropdown.svg'),
    FilterIcon: () =>
      import(/* webpackChunkName: "icons" */ '~/assets/svg/ui/filter.svg'),
    RatingIcon: () =>
      import(/* webpackChunkName: "icons" */ '~/assets/svg/wine/rating.svg'),
    FavoriteIcon: () =>
      import(/* webpackChunkName: "icons" */ '~/assets/svg/wine/favorite.svg')
  },
  data() {
    const searchClient = algoliasearch(
      process.env.algolia.projectId,
      process.env.algolia.apiKey
    )

    const mixin = createServerRootMixin({
      searchClient,
      indexName: process.env.algolia.productIndex,
      $cloneComponent,
      routing: {
        router: nuxtRouter(this.$router),
        stateMapping: singleIndexMapping(process.env.algolia.productIndex)
      }
    })

    return {
      ...mixin.data(),
    }
  },
  provide() {
    return {
      $_ais_ssrInstantSearchInstance: this.instantsearch
    }
  },
  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)

    delete this.$nuxt.context.nuxtState.algoliaState
    delete window.__NUXT__.algoliaState
  },
}
</script>

in nuxt.config.js

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

@adamchipperfield
Copy link

Thanks. So it pushes the filters to the URL as expected after replicating the above. But on reload each facet is empty (as in no values) and the query string is stripped out.

In fact, the blank facets on reload was an issue before adding the routing stuff above. I can only see populated facets when I navigate using the router, reloading the page is where the issue is.

Any clues? Screenshots below:

After navigating via the router things work as expected:
image
image

But then reloading the page after the above results in no filters:
image
image

@podlebar
Copy link
Author

podlebar commented Nov 11, 2021

yep.. see here: #1066 currently it's broken.

what i did now (i guess pretty ugly and it's flickering):

  updated() {
    if (!this.updatedOnce) {
      this.updatedOnce = true

      if (
        this.instantsearch._initialUiState[process.env.algolia.productIndex]
          .refinementList
      ) {
        setTimeout(
          () =>
            this.instantsearch.setUiState(this.instantsearch._initialUiState),
          100
        )
      }
    }
  },

there are a lot of workaround at the moment to make Algolia work with Nuxt SSR.. and somehow i feel like nobody cares that it's broken since a long time.

@federicovezzoli
Copy link

federicovezzoli commented Dec 30, 2021

Hi guys, currently have the same problem with version 4.3.0, since the changes I don't understand where I have to place the $cloneComponent instance with i18n.
Someone can shed some light please?
I'm doing the basic implementation exactly as showed in the docs:
https://www.algolia.com/doc/guides/building-search-ui/going-further/server-side-rendering/vue/#with-nuxt

@podlebar
Copy link
Author

Haroenv added a commit to algolia/instantsearch that referenced this issue Dec 28, 2022
related to algolia/vue-instantsearch#936, this would allow people to link items to the cloned component themselves
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants