Skip to content

Commit 509f32e

Browse files
committed
split into smaller components
1 parent a1d1d94 commit 509f32e

File tree

6 files changed

+319
-0
lines changed

6 files changed

+319
-0
lines changed

src/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import * as React from "react";
22

33
import { isBrowser, isDev } from "./constants.macro";
44

5+
import { warnAboutDeprecation } from "./utils";
6+
7+
export * from "./onEvents";
8+
export * from "./ssrOnly";
9+
export * from "./whenIdle";
10+
export * from "./whenVisible";
11+
512
export type LazyProps = {
613
ssrOnly?: boolean;
714
whenIdle?: boolean;
@@ -59,6 +66,11 @@ const LazyHydrate: React.FunctionComponent<Props> = function(props) {
5966
}
6067
}, []);
6168

69+
React.useEffect(() => {
70+
warnAboutDeprecation({ on, ssrOnly, whenIdle, whenVisible });
71+
// eslint-disable-next-line react-hooks/exhaustive-deps
72+
}, []);
73+
6274
React.useEffect(() => {
6375
if (ssrOnly || hydrated) return;
6476
const cleanupFns: VoidFunction[] = [];

src/onEvents.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as React from "react";
2+
3+
import { defaultStyle, useHydrationState } from "./utils";
4+
5+
type Props = Omit<
6+
React.HTMLProps<HTMLDivElement>,
7+
"dangerouslySetInnerHTML"
8+
> & { on?: (keyof HTMLElementEventMap)[] | keyof HTMLElementEventMap };
9+
10+
function HydrateOn({ children, on, ...rest }: Props) {
11+
const [childRef, hydrated, hydrate] = useHydrationState();
12+
13+
React.useEffect(() => {
14+
if (hydrated) return;
15+
16+
const cleanupFns: VoidFunction[] = [];
17+
18+
function cleanup() {
19+
for (let i = 0; i < cleanupFns.length; i++) {
20+
cleanupFns[i]();
21+
}
22+
}
23+
24+
let events = Array.isArray(on) ? on.slice() : [on];
25+
26+
events.forEach(event => {
27+
childRef.current.addEventListener(event, hydrate, {
28+
once: true,
29+
capture: true,
30+
passive: true
31+
});
32+
cleanupFns.push(() => {
33+
childRef.current.removeEventListener(event, hydrate, { capture: true });
34+
});
35+
});
36+
37+
return cleanup;
38+
}, [hydrated, hydrate, on, childRef]);
39+
40+
if (hydrated) {
41+
return (
42+
<div ref={childRef} style={defaultStyle} children={children} {...rest} />
43+
);
44+
} else {
45+
return (
46+
<div
47+
ref={childRef}
48+
style={defaultStyle}
49+
suppressHydrationWarning
50+
{...rest}
51+
dangerouslySetInnerHTML={{ __html: "" }}
52+
/>
53+
);
54+
}
55+
}
56+
57+
export { HydrateOn };

src/ssrOnly.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as React from "react";
2+
3+
import { defaultStyle, useHydrationState } from "./utils";
4+
5+
type Props = Omit<React.HTMLProps<HTMLDivElement>, "dangerouslySetInnerHTML">;
6+
7+
function SsrOnly({ children, ...rest }: Props) {
8+
const [childRef, hydrated] = useHydrationState();
9+
10+
if (hydrated) {
11+
return (
12+
<div ref={childRef} style={defaultStyle} children={children} {...rest} />
13+
);
14+
} else {
15+
return (
16+
<div
17+
ref={childRef}
18+
style={defaultStyle}
19+
suppressHydrationWarning
20+
{...rest}
21+
dangerouslySetInnerHTML={{ __html: "" }}
22+
/>
23+
);
24+
}
25+
}
26+
27+
export { SsrOnly };

src/utils.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as React from "react";
2+
3+
import { isBrowser, isDev } from "./constants.macro";
4+
5+
// React currently throws a warning when using useLayoutEffect on the server.
6+
const useIsomorphicLayoutEffect = isBrowser
7+
? React.useLayoutEffect
8+
: React.useEffect;
9+
10+
function useHydrationState(): [
11+
React.MutableRefObject<HTMLDivElement>,
12+
boolean,
13+
VoidFunction
14+
] {
15+
const childRef = React.useRef<HTMLDivElement>(null);
16+
17+
const [hydrated, setHydrated] = React.useState(!isBrowser);
18+
19+
useIsomorphicLayoutEffect(() => {
20+
// No SSR Content
21+
if (!childRef.current.hasChildNodes()) {
22+
setHydrated(true);
23+
}
24+
}, []);
25+
26+
const hydrate = React.useCallback(() => {
27+
setHydrated(true);
28+
}, []);
29+
30+
return React.useMemo(() => [childRef, hydrated, hydrate], [
31+
hydrated,
32+
hydrate
33+
]);
34+
}
35+
36+
const defaultStyle: React.CSSProperties = { display: "contents" };
37+
38+
function warnAboutDeprecation({ on, whenIdle, whenVisible, ssrOnly }) {
39+
if (isDev) {
40+
console.warn(
41+
"[%creact-lazy-hydration%c]: Default export is deprecated",
42+
"font-weight:bold",
43+
""
44+
);
45+
if (on != null) {
46+
console.warn(
47+
`To hydrate on events, use the new HydrateOn component
48+
%cimport { HydrateOn } from "react-lazy-hydration";
49+
50+
<HydrateOn on={${JSON.stringify(on)}}>
51+
{children}
52+
</HydrateOn>
53+
`,
54+
"color:red"
55+
);
56+
}
57+
if (whenIdle != null) {
58+
console.warn(
59+
`To hydrate on idle, use the new HydrateOnIdle component
60+
%cimport { HydrateOnIdle } from "react-lazy-hydration";
61+
62+
<HydrateOnIdle>
63+
{children}
64+
</HydrateOnIdle>
65+
`,
66+
"color:red"
67+
);
68+
}
69+
if (whenVisible != null) {
70+
console.warn(
71+
`To hydrate when component becomes visible, use the new HydrateWhenVisible component
72+
%cimport { HydrateWhenVisible } from "react-lazy-hydration";
73+
74+
<HydrateWhenVisible>
75+
{children}
76+
</HydrateWhenVisible>
77+
`,
78+
"color:red"
79+
);
80+
}
81+
if (ssrOnly != null) {
82+
console.warn(
83+
`To skip client side hydration, use the new SsrOnly component
84+
%cimport { SsrOnly } from "react-lazy-hydration";
85+
86+
<SsrOnly>
87+
{children}
88+
</SsrOnly>
89+
`,
90+
"color:red"
91+
);
92+
}
93+
}
94+
}
95+
96+
export { useHydrationState, defaultStyle, warnAboutDeprecation };

src/whenIdle.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as React from "react";
2+
3+
import { defaultStyle, useHydrationState } from "./utils";
4+
5+
type Props = Omit<React.HTMLProps<HTMLDivElement>, "dangerouslySetInnerHTML">;
6+
7+
function HydrateOnIdle({ children, ...rest }: Props) {
8+
const [childRef, hydrated, hydrate] = useHydrationState();
9+
10+
React.useEffect(() => {
11+
if (hydrated) return;
12+
13+
const cleanupFns: VoidFunction[] = [];
14+
15+
function cleanup() {
16+
for (let i = 0; i < cleanupFns.length; i++) {
17+
cleanupFns[i]();
18+
}
19+
}
20+
21+
// @ts-ignore
22+
if (requestIdleCallback) {
23+
// @ts-ignore
24+
const idleCallbackId = requestIdleCallback(hydrate, { timeout: 500 });
25+
cleanupFns.push(() => {
26+
// @ts-ignore
27+
cancelIdleCallback(idleCallbackId);
28+
});
29+
} else {
30+
const id = setTimeout(hydrate, 2000);
31+
cleanupFns.push(() => {
32+
clearTimeout(id);
33+
});
34+
}
35+
36+
return cleanup;
37+
}, [hydrated, hydrate]);
38+
39+
if (hydrated) {
40+
return (
41+
<div ref={childRef} style={defaultStyle} children={children} {...rest} />
42+
);
43+
} else {
44+
return (
45+
<div
46+
ref={childRef}
47+
style={defaultStyle}
48+
suppressHydrationWarning
49+
{...rest}
50+
dangerouslySetInnerHTML={{ __html: "" }}
51+
/>
52+
);
53+
}
54+
}
55+
56+
export { HydrateOnIdle };

src/whenVisible.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as React from "react";
2+
3+
import { defaultStyle, useHydrationState } from "./utils";
4+
5+
type Props = Omit<
6+
React.HTMLProps<HTMLDivElement>,
7+
"dangerouslySetInnerHTML"
8+
> & {
9+
observerOptions?: IntersectionObserverInit;
10+
};
11+
12+
function HydrateWhenVisible({ children, observerOptions, ...rest }: Props) {
13+
const [childRef, hydrated, hydrate] = useHydrationState();
14+
15+
React.useEffect(() => {
16+
if (hydrated) return;
17+
18+
const cleanupFns: VoidFunction[] = [];
19+
20+
function cleanup() {
21+
for (let i = 0; i < cleanupFns.length; i++) {
22+
cleanupFns[i]();
23+
}
24+
}
25+
26+
const io = IntersectionObserver
27+
? new IntersectionObserver(entries => {
28+
// As only one element is observed,
29+
// there is no need to loop over the array
30+
if (entries.length) {
31+
const entry = entries[0];
32+
if (entry.isIntersecting || entry.intersectionRatio > 0) {
33+
hydrate();
34+
}
35+
}
36+
}, observerOptions)
37+
: null;
38+
39+
if (io && childRef.current.childElementCount) {
40+
// As root node does not have any box model, it cannot intersect.
41+
const el = childRef.current.children[0];
42+
io.observe(el);
43+
44+
cleanupFns.push(() => {
45+
io.unobserve(el);
46+
});
47+
48+
return cleanup;
49+
} else {
50+
hydrate();
51+
}
52+
}, [hydrated, hydrate, childRef, observerOptions]);
53+
54+
if (hydrated) {
55+
return (
56+
<div ref={childRef} style={defaultStyle} children={children} {...rest} />
57+
);
58+
} else {
59+
return (
60+
<div
61+
ref={childRef}
62+
style={defaultStyle}
63+
suppressHydrationWarning
64+
{...rest}
65+
dangerouslySetInnerHTML={{ __html: "" }}
66+
/>
67+
);
68+
}
69+
}
70+
71+
export { HydrateWhenVisible };

0 commit comments

Comments
 (0)