Skip to content

Instantly share code, notes, and snippets.

@areknawo
Created August 13, 2021 11:18
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save areknawo/b7673ff99276edd4dee90a0a60b13bfd to your computer and use it in GitHub Desktop.
Save areknawo/b7673ff99276edd4dee90a0a60b13bfd to your computer and use it in GitHub Desktop.
Vue 3 lazy hydration component
<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
Copy link
Author

@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.

@spacedawwwg
Copy link

@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.

image

@areknawo
Copy link
Author

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.

@duannx
Copy link

duannx commented Jun 24, 2022

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