Created
August 13, 2021 11:18
-
-
Save areknawo/b7673ff99276edd4dee90a0a60b13bfd to your computer and use it in GitHub Desktop.
Vue 3 lazy hydration component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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 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?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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.