-
-
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> |
@areknawo I think what's missing is || !wrapper on line 54
if (ssrOnly || hydrated || !wrapper) {
@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 have you got a working demo of this? I can't seem to get it working as 'wrapper' is
nullwhen it hits the watch