question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Nuxt + Routing : stateToRoute rettrigering with an empty state when changing a filter

See original GitHub issue

Bug 🐞

What is the current behavior?

When i add or remove a filter, stateToRoute is triggered twice : the first time normally, then immediately a second times with an empty index. This empty state goes right into my router “write” function, which then remove all my filters and redirect me to my page without any filter.

Does it happens under certain circumstances ?

I dont know, i wonder if its not related to the SSR and/or routing. But i couldnt figure the origin of the problem.

What is the expected behavior?

stateToRoute does not trigger with an empty uiState when there is filters selected.

What is the version you are using?

3.8.1

I didnt make a codesandbox because i couldnt find a nuxt template that is working on codesandbox (all those i found was broke), but if this is crucial i can make one.

I this code example i added a workaround so write does not trigger a redirection when it receive the empty state. The undesired empty uiState look like this: { indexName: {} }, it has a key with my index name and the value is an empty object.

<template>
  <ais-instant-search-ssr>
    <ais-configure :hits-per-page.camel="50" />
    <ProductFilters>
      <ais-search-box />
      <ais-refinement-list attribute="product.categories.name" />
    </ProductFilters>
    <div class="list">
      <ais-stats>
        <div slot-scope="{ nbHits }">
          <ProductCount :count="nbHits" />
        </div>
      </ais-stats>
      <ais-infinite-hits
        :transform-items="formatHits"
      >
        <div slot-scope="{ items, refineNext, isLastPage }">
          <ProductList
            :products="items.map(item => item.formattedProduct)"
            :is-show-more-button-showing="Boolean(items.length) && !isLastPage"
            :on-show-more-click="refineNext"
          />
        </div>
      </ais-infinite-hits>
    </div>
  </ais-instant-search-ssr>
</template>

<script>
import {
  createRouteFromRouteState,
  createRouteStateFromRoute,
  createURLFromRouteState,
  formatProducts,
  routeToState,
  stateToRoute,
} from '~/helpers/algolia'
import algoliasearch from 'algoliasearch/lite'
import { createServerRootMixin } from 'vue-instantsearch'

const searchClient = algoliasearch(
  process.env.ALGOLIA_APP_ID,
  process.env.ALGOLIA_API_KEY
)

function nuxtRouter (vueRouter) {
  return {
    read () {
      return createRouteStateFromRoute(vueRouter.currentRoute)
    },
    write (routeState) {
      if (
        routeState.disableWriteBuggyState ||
        this.createURL(routeState) === this.createURL(this.read())
      ) {
        return
      }

      const fullPath = vueRouter.resolve({
        query: routeState,
      }).href
      const route = createRouteFromRouteState(fullPath, routeState)
      vueRouter.replace(route)
    },
    createURL (routeState) {
      const fullPath = vueRouter.resolve({
        query: routeState,
      }).href

      return createURLFromRouteState(fullPath, routeState)
    },
    // call this callback whenever the URL changed externally
    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)
    },
    // remove any listeners
    dispose () {
      if (typeof window === 'undefined') { return }

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

export default {
  provide () {
    return {
      $_ais_ssrInstantSearchInstance: this.instantsearch,
    }
  },
  data () {
    const mixin = createServerRootMixin({
      searchClient,
      indexName: process.env.ALGOLIA_PRODUCT_INDEX_NAME,
      routing: {
        router: nuxtRouter(this.$router),
        stateMapping: { stateToRoute, routeToState },
      },
    })

    return {
      ...mixin.data(),
    }
  },
  serverPrefetch () {
    return this.instantsearch.findResultsState(this).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
  },
  methods: {
    formatHits: formatProducts,
  },
}
</script>

<style scoped>
.list {
  grid-area: list;
}
</style>
// ~/helpers/algolia
import qs from 'qs'

const PRODUCT_CATEGORIES = 'product.categories.name'

const formatSizes = (sizes) => {
  if (!sizes) { return '' }

  return sizes
    .map(size => size.name)
    .reduce((acc, size, index) => {
      if (index === 0) { return acc + size }

      return acc + ',' + size
    }, '')
}

export const formatProducts = items => items.map((item) => {
  const id = item?.objectID ?? ''
  const name = item?.product?.content?.name ?? ''
  const image = item?.product?.images?.details[0] ?? ''
  const price = item?.product?.pricing?.current ?? -1
  const sizes = formatSizes(item?.product?.measures?.sizes)

  return {
    ...item,
    formattedProduct: {
      id,
      name,
      image,
      price,
      sizes,
    },
  }
})

export const getCategoriesFromPath = (path) => {
  const categories = /(\/products\/)(.*?)(?:[\/?]|$)/.exec(path)

  if (!categories) return []

  return categories[2]
    ? categories[2].split('+').map(capitalizeFirstLetter)
    : []
}

const capitalizeFirstLetter = string => string
  .charAt(0)
  .toUpperCase() +
  string.slice(1)

export const createRouteStateFromRoute = (route) => {
  const categories = getCategoriesFromPath(route.fullPath)

  return {
    categories,
    q: route.query.text,
    page: route.query.page,
  }
}

export const createRouteFromRouteState = (fullPath, routeState) => {
  const url = createURLFromRouteState(fullPath, routeState)

  return {
    query: {
      text: routeState.q,
      page: routeState.page,
    },
    fullPath: url,
    path: url.split('?')[0],
  }
}

export const createURLFromRouteState = (fullPath, routeState) => {
  const myParams = qs.stringify({ categories: routeState.categories, text: routeState.q, page: routeState.page }, {
    addQueryPrefix: true,
    arrayFormat: 'repeat',
    format: 'RFC3986',
  })

  const base = '/products/'

  if (!routeState?.categories?.length) {
    if (!myParams) {
      return base
    }

    return base + myParams
  }

  const result = base +
    routeState?.categories.map(c => c.toLowerCase()).join('+') +
    myParams

  return result
}

export const stateToRoute = (uiState) => {
  console.log('uiState', uiState)
  const indexUiState = uiState[process.env.ALGOLIA_PRODUCT_INDEX_NAME]

  if (Object.entries(indexUiState).length === 0) {
    return { disableWriteBuggyState: true }
  }

  const categories = indexUiState?.refinementList[PRODUCT_CATEGORIES] ?? []

  return {
    q: indexUiState?.query,
    categories,
    page: indexUiState?.page,
  }
}

export const routeToState = routeState => ({
  [process.env.ALGOLIA_PRODUCT_INDEX_NAME]: {
    query: routeState.q,
    refinementList: {
      [PRODUCT_CATEGORIES]: routeState.categories,
    },
    page: routeState.page,
  },
})

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
louislebraultcommented, Jul 29, 2021

Here’s my fix :

function nuxtRouter (vueRouter) {
   // ...
    write (routeState) {
      // history exists only on client-side
      if (typeof history === 'undefined') return

      const fullPath = vueRouter.resolve({
        query: routeState,
      }).href

      const route = createRouteFromRouteState(fullPath, routeState)
      
      // route variable shape look like this : { path: String, fullPath: String, query: Object }
      // I think its not mandatory to pass this object to replaceState, the important parameter is the third one
      // https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
      history.replaceState(route, '', route.fullPath)
    },
    // ...

Its the simplest fix i found based on the documentation example, but there’s probably better ways to do it (maybe not using vueRouter instance at all, since its only used to get the base url ?).

1reaction
louislebraultcommented, Jul 28, 2021

Since its directly related to vue-router behaviors i would expect to find it here i think : https://www.algolia.com/doc/guides/building-search-ui/going-further/routing-urls/vue/#combining-with-vue-router

Happy to hear that my recent struggles will lead into a tiny improvement in the documentation 😌

Btw, in Nuxt app case, historyRouter (from instantsearch.js/es/lib/routers) wont work out of the box since it depends on global object window, which is only accessible from the client-side (nothing but importing it in a component is enough to make it crash during server-side render).

Read more comments on GitHub >

github_iconTop Results From Across the Web

Empty uiState after route change - Algolia Community
The issue I'm having is that after a navigation event (e.g. clicking on an item in an hierarchical menu widget or clicking a...
Read more >
The router Property - Nuxt
The router property lets you customize Nuxt router. ... You may want to change the separator between route names that Nuxt uses.
Read more >
Issues · algolia/vue-instantsearch · GitHub
Nuxt + Routing : stateToRoute rettrigering with an empty state when changing a filter. #1025 opened Jul 23, 2021 by louislebrault.
Read more >
Creating Dynamic Routes in a Nuxt Application | CSS-Tricks
js , along with the UI state, and an empty array for the cart. import data from '~/static/storedata.json' export const state ...
Read more >
nuxt filters的解答,GITHUB、STACKOVERFLOW ... - 工程師的救星
nuxt filters 在How to Create a Filter Component for NuxtJS Blogs - YouTube 的評價; nuxt filters 在Build an advanced search and filter with...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found