Nuxt + Routing : stateToRoute rettrigering with an empty state when changing a filter
See original GitHub issueBug 🐞
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:
- Created 2 years ago
- Comments:5 (2 by maintainers)
Top GitHub Comments
Here’s my fix :
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 ?).
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
(frominstantsearch.js/es/lib/routers
) wont work out of the box since it depends on global objectwindow
, 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).