Please help me debug this always null ref value!
Quick summary:
I have a horizontally scrolling row in a pair of parent/child components. The parent is handling infinite scrolling, and within it is a list that is meant to be replenished. The goal is to register an IntersectionObserver within InfiniteScroll.vue
(parent), and retrieve a template ref placed after the terminal list element in List.vue
(child). Ideally, when the terminalRef
appears in the DOM, the IntersectionObserver fires it’s callback, fetching and refilling the list.
So IntersectionObserver should be able to retrieve the child’s terminalRef.value
. According to vue documentation, this is achievable with slots using the render and expose methods, but I am struggling to find a comprehensive example that combines passing a template ref through a slot using render
syntax. After much fiddling, I have gotten most of it functional, but still the child ref from List.vue
remains null in InfiniteScroll.vue
.
Note: I followed the instructions in the docs for Typing Component Template Refs.
Here’s the code:
// InfinitScroll.vue
<script lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, h, defineComponent } from 'vue'
import List from '../List/List.vue'
export default defineComponent({
components: { List },
emits: ['infinite'],
setup(props, { slots, emit }) {
const scrollContainer = ref<HTMLElement|null>(null)
const ListInstance = ref<InstanceType<typeof List>|null>(null)
const getTerminalRef = () => ListInstance.value?.$refs.terminalRef as HTMLDivElement|null
const observer = new IntersectionObserver((entries) => {
const entry = entries[0]
if (entry.isIntersecting) {
emit('infinite')
}
}, { root: scrollContainer.value, threshold: 0.9 })
onMounted(() => {
if (ListInstance.value) { // always null, but the ref is visible in vue dev tools
debugger // <---- Never stops here
const ref = ListInstance.value.$refs.terminalRef as HTMLDivElement
if (ref) {
observer.observe(ref)
}
}
return () => h(
'div',
{ ref: scrollContainer, class: 'scroll-container' },
h('slot', { name: 'List', ref: 'terminalRef' }, slots.default && slots.default())
)
}
})
</script>
// List.vue
import { ref, h, defineComponent, onMounted } from 'vue'
import { type UIshow } from '../../client-types'
import ListItem from './ListItem.vue'
export default defineComponent({
components: { ListItem },
props: ['genre', 'shows'],
setup({ genre, shows }: { genre: string, shows: Map<string, UIshow> }, { expose }) {
const terminalRef = ref<HTMLDivElement|null>(null)
expose({ terminalRef })
return () => h('div', { class: 'genre-row' }, { default: () => [
h('h2', { class: 'title' }, [genre]),
h('ul', { class: 'primary' }, [
...Array.from(shows).map(
([sortingName, show]) => {
return h('li', { key: sortingName }, [
h(ListItem, { show })
])
}),
h('li', null, h('div', { ref: terminalRef }, '.'))
])
]})
}
})
</script>
// Lists.vue iterates over multiple InfiniteScroll/List pairs
<template>
<template v-if="genreMap.size">
<div v-for="[ genre, shows ] of genreMap" :key="genre" class="all-lists">
<template v-if="shows.size">
<InfiniteScroll @infinite="loadPage">
<List
:genre="genre"
:shows="shows"
/>
</InfiniteScroll>
</template>
</div>
</template>
<template v-else>
<h1 class="title">Loading...</h1>
</template>
</template>