DerYeger / yeger

Monorepo for @yeger/ NPM packages
MIT License
316 stars 24 forks source link

Bug: Nuxt 3 navigation causes grid errors #224

Closed EmilyYond closed 11 months ago

EmilyYond commented 1 year ago

Affected Packages

Description

I am getting the following warning "Unhandled error during execution of watcher callback" and then error "TypeError: Cannot read properties of null (reading 'parentNode')" which only occur when I use MasonryWall. If I remove it for a plain css grid, there are no errors.

Screenshot 2023-11-15 at 10 20 33

For context, I have a masonry grid on my home page that has infinite scroll (so when you scroll to the bottom the next page is triggered and added to the list). Some of my other pages also contain the masonry grid so I am reusing the same component on these different pages.

Its really hard to replicate, as it is an inconsistent error but these are times I have had the error:

  1. Going to a page and then clicking the browser back button to the home page This doesn't happen every single time, but it does occur regularly
  2. After going to another page and back to the home page, scrolling down to trigger the pagination If the home page does work immediately after coming back to it, scrolling down and triggering our pagination then displays the error and the grid goes white.
  3. Going from the home page to another page that uses the same masonry grid component This causes the other page to error and never load. I have tried keying these components to define that they are different in case it was a caching issue, but that didn't help. For now, I have replaced the grid on the other pages with a plain css grid so only the home page uses MasonryWall and those other pages don't error anymore but I need them to be masonry too.

Reproduction

I am using Nuxt 3 with the following versions of packages (only including the ones I think might be relevant)

"nuxt": "^3.8.0",
"postcss": "^8.4.24",
"sass": "^1.63.6",
"sass-loader": "^13.3.2",
"tailwindcss": "^3.3.2",
"@types/node": "^20.8.2",
"@yeger/vue-masonry-wall": "^5.0.2",
"typescript": "^5.1.6",
...
"engines": {
    "node": ">=18.x.x",
    "npm": ">=6.0.0"
},
"packageManager": "yarn@4.0.1"

Then I define the component in a plugin (masonry.client.ts - I have tried as masonry.ts too)

// https://www.npmjs.com/package/@yeger/vue-masonry-wall
import { defineNuxtPlugin } from '#app'
import MasonryWall from '@yeger/vue-masonry-wall'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(MasonryWall)
})

The my masonry component (masonry.client.vue - I have also tried masonry.vue)

<template>
  <div v-if="list" class="layouts-masonry">
    <MasonryWall
      :items="list"
      :ssr-columns="ssrColumns"
      :column-width="width"
      :gap="gap"
    >
      <template #default="{ item, index }">
        <div
          ref="masonryItems"
          class="masonry-cell"
          :class="[
            `item-${index + 1}`,
            {
              loaded: loaded,
            },
          ]"
        >
          <Thumbnail
            :item="item"
            class="masonry-item"
          />
        </div>
      </template>
    </MasonryWall>
  </div>
</template>

<script setup>
// ----- Props -----
const props = defineProps({
  list: {
    type: Array,
    default: null,
  },
  gap: {
    type: Number,
    default: 15,
  },
  width: {
    type: Number,
    default: 300,
  },
  ssrColumns: {
    type: Number,
    default: 2,
  }
})

let loaded = ref(false)
let masonry = ref()
let masonryItems = ref([])

setTimeout(() => {
  loaded.value = true
}, 200)
</script>

<style lang="scss" scoped>
.masonry-cell {
  opacity: 0;
  transition: 0;
  &.loaded {
    opacity: 1;
    transition: 0.75s ease;

    @for $i from 1 through 24 {
      &.item-#{$i} {
        transition-delay: calc(($i * 0.15s) - 0.15s);
      }
    }
  }
}
</style>

And then a simplified version of my home page, with some sensitive information removed, which is currently the only place calling it (although I need to be able to call it on other pages too)

<template>
  <div v-if="posts" class="homepage">
    <div class="container">
      <GeneralSearch />
      <button @click="showMap = !showMap">Show/Hide Map</button>
      <div class="min-h-[50vh]">
        <div
          v-if="posts && posts.length"
          id="results"
          ref="resultsElem"
          class="mb-small relative"
        >
          <LayoutsResultsMap v-if="showMap" :posts="posts" />
          <LayoutsMasonry key="homepage" v-else :list="posts" />

          <Transition name="fade">
            <UtilitySimpleLoader v-if="loading" />
          </Transition>
        </div>

        <div v-else class="pt-small pb-large">
          <p>
            Uh oh, no results found. Try clearing some of your search items or
            hit reset to start again
          </p>
        </div>
      </div>
    </div>
    <UtilityLoader v-if="pending || postsLoading" />
  </div>
  <ErrorPage v-else />
</template>

<script setup>
let posts = ref(null)
let initialPosts = ref(null)
let page = ref(1)
let nbPages = ref(1)
let nbHits = ref(30)
let showMap = ref(false)
let loading = ref(false)
let postsLoading = ref(true)
let resultsElem = ref()

// ----- Page setup fetch ----
let {
  data: res,
  error,
  pending,
} = await useFetch('***').catch((e) => {
  console.error(e)
})

if (res.value?.statusCode == 200 && res.value.data) {
  posts.value = res.value.data.results?.hits
  page.value = res.value.data.results?.page
  nbPages.value = res.value.data.results?.nbPages
  nbHits.value = res.value.data.results?.nbHits
  postsLoading.value = false
} else {
  console.error('Error getting home page: ', res.value)
}

// ----- Methods -----
async function scrollTrigger() {
  if (!loading.value && page.value != nbPages.value - 1) {
    let el = resultsElem.value
    const bottom = el?.getBoundingClientRect().bottom

    if (window.innerHeight > bottom) {
      console.log('bottom is visible - trigger pagination')
      loading.value = true
      page.value++
        let { data: res, error } = await useFetch('***')
        if (
          res.value?.statusCode == 200 &&
          res.value.data &&
          res.value.data.results?.hits
        ) {
          posts.value = posts.value.concat(res.value.data.results?.hits)
          page.value = res.value.data.results?.page
          nbPages.value = res.value.data.results?.nbPages
          nbHits.value = res.value.data.results?.nbHits
          loading.value = false
        } else {
          console.error('Error getting next page: ', res.value)
        }
    }
  }
}

// ----- Lifecycle -----
onMounted(() => {
  if (process.client) {
    document.addEventListener('scroll', scrollTrigger)
  }
})
onBeforeUnmount(() => {
  if (process.client) {
    document.removeEventListener('scroll', scrollTrigger)
  }
})
</script>

Additional context

Any help would be greatly appreciated! Thank you

Preferences

DerYeger commented 12 months ago

This seems to be a Nuxt issue. I cannot identify a source within this component in the stack trace. Without an executable reproduction I cannot help.

EmilyYond commented 12 months ago

Hi @DerYeger - thank you for looking into this, I will open it as a nuxt issue instead