-
-
Save areknawo/b7673ff99276edd4dee90a0a60b13bfd to your computer and use it in GitHub Desktop.
<script lang="ts"> | |
import { defineComponent, onMounted, PropType, ref, watch } from "vue"; | |
type VoidFunction = () => void; | |
const isBrowser = () => { | |
return typeof window === "object"; | |
}; | |
export default defineComponent({ | |
props: { | |
ssrOnly: Boolean, | |
whenIdle: Boolean, | |
whenVisible: [Boolean, Object] as PropType< | |
boolean | IntersectionObserverInit | |
>, | |
didHydrate: Function as PropType<() => void>, | |
promise: Object as PropType<Promise<any>>, | |
on: [Array, String] as PropType< | |
(keyof HTMLElementEventMap)[] | keyof HTMLElementEventMap | |
>, | |
}, | |
setup(props) { | |
const noOptions = | |
!props.ssrOnly && | |
!props.whenIdle && | |
!props.whenVisible && | |
!props.on?.length && | |
!props.promise; | |
const wrapper = ref<Element | null>(null); | |
const hydrated = ref(noOptions || !isBrowser()); | |
const hydrate = () => { | |
hydrated.value = true; | |
}; | |
onMounted(() => { | |
if (wrapper.value && !wrapper.value.hasChildNodes()) { | |
hydrate(); | |
} | |
}); | |
watch( | |
hydrated, | |
(hydrate) => { | |
if (hydrate && props.didHydrate) props.didHydrate(); | |
}, | |
{ immediate: true } | |
); | |
watch( | |
[() => props, wrapper, hydrated], | |
( | |
[{ on, promise, ssrOnly, whenIdle, whenVisible }, wrapper, hydrated], | |
_, | |
onInvalidate | |
) => { | |
if (ssrOnly || hydrated) { | |
return; | |
} | |
const cleanupFns: VoidFunction[] = []; | |
const cleanup = () => { | |
cleanupFns.forEach((fn) => { | |
fn(); | |
}); | |
}; | |
if (promise) { | |
promise.then(hydrate, hydrate); | |
} | |
if (whenVisible) { | |
if (wrapper && typeof IntersectionObserver !== "undefined") { | |
const observerOptions = | |
typeof whenVisible === "object" | |
? whenVisible | |
: { | |
rootMargin: "250px", | |
}; | |
const io = new IntersectionObserver((entries) => { | |
entries.forEach((entry) => { | |
if (entry.isIntersecting || entry.intersectionRatio > 0) { | |
hydrate(); | |
} | |
}); | |
}, observerOptions); | |
io.observe(wrapper); | |
cleanupFns.push(() => { | |
io.disconnect(); | |
}); | |
} else { | |
return hydrate(); | |
} | |
} | |
if (whenIdle) { | |
if (typeof window.requestIdleCallback !== "undefined") { | |
const idleCallbackId = window.requestIdleCallback(hydrate, { | |
timeout: 500, | |
}); | |
cleanupFns.push(() => { | |
window.cancelIdleCallback(idleCallbackId); | |
}); | |
} else { | |
const id = setTimeout(hydrate, 2000); | |
cleanupFns.push(() => { | |
clearTimeout(id); | |
}); | |
} | |
} | |
if (on) { | |
const events = ([] as Array<keyof HTMLElementEventMap>).concat(on); | |
events.forEach((event) => { | |
wrapper?.addEventListener(event, hydrate, { | |
once: true, | |
passive: true, | |
}); | |
cleanupFns.push(() => { | |
wrapper?.removeEventListener(event, hydrate, {}); | |
}); | |
}); | |
} | |
onInvalidate(cleanup); | |
}, | |
{ immediate: true } | |
); | |
return { | |
wrapper, | |
hydrated, | |
}; | |
}, | |
}); | |
</script> | |
<template> | |
<div ref="wrapper" :style="{ display: 'contents' }" v-if="hydrated"> | |
<slot></slot> | |
</div> | |
<div ref="wrapper" v-else></div> | |
</template> |
@spacedawwwg line 54 is ok. Proper wrapper
checks are implemented after it. If you haven't seen it already, check out this related post to learn more.
@areknawo I have - followed the whole thing, implemented as above.
But "when visible" wasn't working. wrapper
was always null
on first watch hit - as such, it gets to line 69 and always skips the intersection observer and hydrates instantly.
Now I see what you mean. Initially, I thought you mean the code was throwing errors or something. 😅
I might have followed react-lazy-hydration too closely and not noticed this issue.
I'll do some testing tomorrow and likely apply your fix. Thanks for letting me know.
When you set hydrated = true
on the client, does the client re-render the HTML or does it use HTML from the server? How did you test it?
@areknawo I think what's missing is
|| !wrapper
on line 54