Skip to content

split into smaller components #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import * as React from "react";

import { isBrowser, isDev } from "./constants.macro";
import { warnAboutDeprecation } from "./utils";

export * from "./onEvents";
export * from "./ssrOnly";
export * from "./whenIdle";
export * from "./whenVisible";

export type LazyProps = {
ssrOnly?: boolean;
Expand All @@ -18,7 +24,7 @@ const event = "hydrate";

const io =
isBrowser && IntersectionObserver
? new IntersectionObserver(
? /*#__PURE__*/ new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting || entry.intersectionRatio > 0) {
Expand Down Expand Up @@ -60,6 +66,9 @@ const LazyHydrate: React.FunctionComponent<Props> = function(props) {
}, []);

React.useEffect(() => {
if (isDev) {
warnAboutDeprecation({ on, ssrOnly, whenIdle, whenVisible });
}
if (ssrOnly || hydrated) return;
const cleanupFns: VoidFunction[] = [];
function cleanup() {
Expand Down
59 changes: 59 additions & 0 deletions src/onEvents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from "react";

import { defaultStyle, useHydrationState } from "./utils";

type Props = Omit<
React.HTMLProps<HTMLDivElement>,
"dangerouslySetInnerHTML"
> & { on: (keyof HTMLElementEventMap)[] | keyof HTMLElementEventMap };

function HydrateOn({ children, on, ...rest }: Props) {
const [childRef, hydrated, hydrate] = useHydrationState();

React.useEffect(() => {
if (hydrated) return;

const cleanupFns: VoidFunction[] = [];

function cleanup() {
for (let i = 0; i < cleanupFns.length; i++) {
cleanupFns[i]();
}
}

let events = Array.isArray(on) ? on.slice() : [on];

const domElement = childRef.current!;

events.forEach(event => {
domElement.addEventListener(event, hydrate, {
once: true,
capture: true,
passive: true
});
cleanupFns.push(() => {
domElement.removeEventListener(event, hydrate, { capture: true });
});
});

return cleanup;
}, [hydrated, hydrate, on, childRef]);

if (hydrated) {
return (
<div ref={childRef} style={defaultStyle} children={children} {...rest} />
);
} else {
return (
<div
ref={childRef}
style={defaultStyle}
suppressHydrationWarning
{...rest}
dangerouslySetInnerHTML={{ __html: "" }}
/>
);
}
}

export { HydrateOn };
27 changes: 27 additions & 0 deletions src/ssrOnly.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from "react";

import { defaultStyle, useHydrationState } from "./utils";

type Props = Omit<React.HTMLProps<HTMLDivElement>, "dangerouslySetInnerHTML">;

function SsrOnly({ children, ...rest }: Props) {
const [childRef, hydrated] = useHydrationState();

if (hydrated) {
return (
<div ref={childRef} style={defaultStyle} children={children} {...rest} />
);
} else {
return (
<div
ref={childRef}
style={defaultStyle}
suppressHydrationWarning
{...rest}
dangerouslySetInnerHTML={{ __html: "" }}
/>
);
}
}

export { SsrOnly };
101 changes: 101 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as React from "react";

import { isBrowser } from "./constants.macro";

// React currently throws a warning when using useLayoutEffect on the server.
const useIsomorphicLayoutEffect = isBrowser
? React.useLayoutEffect
: React.useEffect;

function useHydrationState(): [
React.MutableRefObject<HTMLDivElement | null>,
boolean,
VoidFunction
] {
const childRef = React.useRef<HTMLDivElement>(null);

const [hydrated, setHydrated] = React.useState(!isBrowser);

useIsomorphicLayoutEffect(() => {
// No SSR Content
if (!childRef.current!.hasChildNodes()) {
setHydrated(true);
}
}, []);

const hydrate = React.useCallback(() => {
setHydrated(true);
}, []);

return [childRef, hydrated, hydrate];
}

const defaultStyle: React.CSSProperties = { display: "contents" };

function warnAboutDeprecation({
on,
whenIdle,
whenVisible,
ssrOnly
}: {
on?: string | string[];
whenIdle?: boolean;
whenVisible?: boolean;
ssrOnly?: boolean;
}) {
console.warn(
"[%creact-lazy-hydration%c]: Default export is deprecated",
"font-weight:bold",
""
);
if (on != null) {
console.warn(
`To hydrate on events, use the new HydrateOn component
%cimport { HydrateOn } from "react-lazy-hydration";

<HydrateOn on={${JSON.stringify(on)}}>
{children}
</HydrateOn>
`,
"color:red"
);
}
if (whenIdle != null) {
console.warn(
`To hydrate on idle, use the new HydrateOnIdle component
%cimport { HydrateOnIdle } from "react-lazy-hydration";

<HydrateOnIdle>
{children}
</HydrateOnIdle>
`,
"color:red"
);
}
if (whenVisible != null) {
console.warn(
`To hydrate when component becomes visible, use the new HydrateWhenVisible component
%cimport { HydrateWhenVisible } from "react-lazy-hydration";

<HydrateWhenVisible>
{children}
</HydrateWhenVisible>
`,
"color:red"
);
}
if (ssrOnly != null) {
console.warn(
`To skip client side hydration, use the new SsrOnly component
%cimport { SsrOnly } from "react-lazy-hydration";

<SsrOnly>
{children}
</SsrOnly>
`,
"color:red"
);
}
}

export { useHydrationState, defaultStyle, warnAboutDeprecation };
46 changes: 46 additions & 0 deletions src/whenIdle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from "react";

import { defaultStyle, useHydrationState } from "./utils";

type Props = Omit<React.HTMLProps<HTMLDivElement>, "dangerouslySetInnerHTML">;

function HydrateOnIdle({ children, ...rest }: Props) {
const [childRef, hydrated, hydrate] = useHydrationState();

React.useEffect(() => {
if (hydrated) return;

// @ts-ignore
if (requestIdleCallback) {
// @ts-ignore
const idleCallbackId = requestIdleCallback(hydrate, { timeout: 500 });
return () => {
// @ts-ignore
cancelIdleCallback(idleCallbackId);
};
} else {
const id = setTimeout(hydrate, 2000);
return () => {
clearTimeout(id);
};
}
}, [hydrated, hydrate]);

if (hydrated) {
return (
<div ref={childRef} style={defaultStyle} children={children} {...rest} />
);
} else {
return (
<div
ref={childRef}
style={defaultStyle}
suppressHydrationWarning
{...rest}
dangerouslySetInnerHTML={{ __html: "" }}
/>
);
}
}

export { HydrateOnIdle };
96 changes: 96 additions & 0 deletions src/whenVisible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as React from "react";

import { defaultStyle, useHydrationState } from "./utils";

type Props = Omit<
React.HTMLProps<HTMLDivElement>,
"dangerouslySetInnerHTML"
> & {
observerOptions?: IntersectionObserverInit;
};

const hydrationEvent = "hydrate";

function HydrateWhenVisible({ children, observerOptions, ...rest }: Props) {
const [childRef, hydrated, hydrate] = useHydrationState();

React.useEffect(() => {
if (hydrated) return;

const io = createIntersectionObserver(observerOptions);

// As root node does not have any box model, it cannot intersect.
const domElement = childRef.current!.firstElementChild;

if (io && domElement) {
io.observe(domElement);

domElement.addEventListener(hydrationEvent, hydrate, {
once: true,
capture: true,
passive: true
});

return () => {
io.unobserve(domElement);

domElement.removeEventListener(hydrationEvent, hydrate, {
capture: true
});
};
} else {
hydrate();
}
}, [hydrated, hydrate, childRef, observerOptions]);

if (hydrated) {
return (
<div ref={childRef} style={defaultStyle} children={children} {...rest} />
);
} else {
return (
<div
ref={childRef}
style={defaultStyle}
suppressHydrationWarning
{...rest}
dangerouslySetInnerHTML={{ __html: "" }}
/>
);
}
}

const observerCache = new WeakMap<
IntersectionObserverInit,
IntersectionObserver
>();

const defaultOptions = {};

function createIntersectionObserver(
observerOptions?: IntersectionObserverInit
) {
if (!IntersectionObserver) return null;

observerOptions = observerOptions || defaultOptions;

let io = observerCache.get(observerOptions);

if (!io) {
observerCache.set(
observerOptions,
(io = new IntersectionObserver(entries => {
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (entry.isIntersecting || entry.intersectionRatio > 0) {
entry.target.dispatchEvent(new CustomEvent(hydrationEvent));
}
}
}, observerOptions))
);
}

return io;
}

export { HydrateWhenVisible };