Skip to content

Commit 716e412

Browse files
author
Hadeeb Farhan
committed
Refactor
* Create an IntersectionObserver per component * Add option to modify IntersectionObserver parameters * Add option to override wrapper element * Capture ref value in effects, closes #30
1 parent b06578e commit 716e412

File tree

1 file changed

+75
-57
lines changed

1 file changed

+75
-57
lines changed

src/index.tsx

Lines changed: 75 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,33 @@ import { isBrowser, isDev } from "./constants.macro";
55
export type LazyProps = {
66
ssrOnly?: boolean;
77
whenIdle?: boolean;
8-
whenVisible?: boolean;
9-
noWrapper?: boolean;
8+
whenVisible?: boolean | IntersectionObserverInit;
9+
noWrapper?: boolean | keyof JSX.IntrinsicElements;
1010
didHydrate?: VoidFunction;
1111
promise?: Promise<any>;
1212
on?: (keyof HTMLElementEventMap)[] | keyof HTMLElementEventMap;
1313
children: React.ReactElement;
1414
};
1515

16-
type Props = Omit<React.HTMLProps<HTMLDivElement>, "dangerouslySetInnerHTML"> &
16+
type Props = Omit<React.HTMLProps<HTMLElement>, "dangerouslySetInnerHTML"> &
1717
LazyProps;
1818

1919
type VoidFunction = () => void;
2020

21-
const event = "hydrate";
22-
23-
const io =
24-
isBrowser && typeof IntersectionObserver !== "undefined"
25-
? new IntersectionObserver(
26-
entries => {
27-
entries.forEach(entry => {
28-
if (entry.isIntersecting || entry.intersectionRatio > 0) {
29-
entry.target.dispatchEvent(new CustomEvent(event));
30-
}
31-
});
32-
},
33-
{
34-
rootMargin: "250px"
35-
}
36-
)
37-
: null;
38-
3921
// React currently throws a warning when using useLayoutEffect on the server.
4022
const useIsomorphicLayoutEffect = isBrowser
4123
? React.useLayoutEffect
4224
: React.useEffect;
4325

26+
function reducer() {
27+
return true;
28+
}
29+
4430
function LazyHydrate(props: Props) {
45-
const childRef = React.useRef<HTMLDivElement>(null);
31+
const childRef = React.useRef<HTMLElement>(null);
4632

4733
// Always render on server
48-
const [hydrated, setHydrated] = React.useState(!isBrowser);
34+
const [hydrated, hydrate] = React.useReducer(reducer, !isBrowser);
4935

5036
const {
5137
noWrapper,
@@ -76,27 +62,63 @@ function LazyHydrate(props: Props) {
7662
useIsomorphicLayoutEffect(() => {
7763
// No SSR Content
7864
if (!childRef.current.hasChildNodes()) {
79-
setHydrated(true);
65+
hydrate();
8066
}
8167
}, []);
8268

69+
React.useEffect(() => {
70+
if (hydrated && didHydrate) {
71+
didHydrate();
72+
}
73+
// eslint-disable-next-line react-hooks/exhaustive-deps
74+
}, [hydrated]);
75+
8376
React.useEffect(() => {
8477
if (ssrOnly || hydrated) return;
78+
const rootElement = childRef.current;
79+
8580
const cleanupFns: VoidFunction[] = [];
8681
function cleanup() {
87-
while (cleanupFns.length) {
88-
cleanupFns.pop()();
89-
}
90-
}
91-
function hydrate() {
92-
setHydrated(true);
93-
if (didHydrate) didHydrate();
82+
cleanupFns.forEach(fn => {
83+
fn();
84+
});
9485
}
9586

9687
if (promise) {
97-
promise.then(hydrate).catch(hydrate);
88+
promise.then(hydrate, hydrate);
9889
}
9990

91+
if (whenVisible) {
92+
const element = noWrapper
93+
? rootElement
94+
: // As root node does not have any box model, it cannot intersect.
95+
rootElement.firstElementChild;
96+
97+
if (element && typeof IntersectionObserver !== "undefined") {
98+
const observerOptions =
99+
typeof whenVisible === "object"
100+
? whenVisible
101+
: {
102+
rootMargin: "250px"
103+
};
104+
105+
const io = new IntersectionObserver(entries => {
106+
entries.forEach(entry => {
107+
if (entry.isIntersecting || entry.intersectionRatio > 0) {
108+
hydrate();
109+
}
110+
});
111+
}, observerOptions);
112+
113+
io.observe(element);
114+
115+
cleanupFns.push(() => {
116+
io.disconnect();
117+
});
118+
} else {
119+
return hydrate();
120+
}
121+
}
100122
if (whenIdle) {
101123
// @ts-ignore
102124
if (typeof requestIdleCallback !== "undefined") {
@@ -114,53 +136,49 @@ function LazyHydrate(props: Props) {
114136
}
115137
}
116138

117-
let events = Array.isArray(on) ? on.slice() : [on];
118-
119-
if (whenVisible) {
120-
if (io && childRef.current.childElementCount) {
121-
// As root node does not have any box model, it cannot intersect.
122-
const el = childRef.current.children[0];
123-
io.observe(el);
124-
events.push(event as keyof HTMLElementEventMap);
125-
126-
cleanupFns.push(() => {
127-
io.unobserve(el);
128-
});
129-
} else {
130-
return hydrate();
131-
}
132-
}
139+
const events = ([] as Array<keyof HTMLElementEventMap>).concat(on);
133140

134141
events.forEach(event => {
135-
childRef.current.addEventListener(event, hydrate, {
142+
rootElement.addEventListener(event, hydrate, {
136143
once: true,
137-
capture: true,
138144
passive: true
139145
});
140146
cleanupFns.push(() => {
141-
childRef.current.removeEventListener(event, hydrate, { capture: true });
147+
rootElement.removeEventListener(event, hydrate, {});
142148
});
143149
});
144150

145151
return cleanup;
146-
}, [hydrated, on, ssrOnly, whenIdle, whenVisible, didHydrate, promise]);
152+
}, [
153+
hydrated,
154+
on,
155+
ssrOnly,
156+
whenIdle,
157+
whenVisible,
158+
didHydrate,
159+
promise,
160+
noWrapper
161+
]);
162+
163+
const WrapperElement = ((typeof noWrapper === "string"
164+
? noWrapper
165+
: "div") as unknown) as React.FC<React.HTMLProps<HTMLElement>>;
147166

148167
if (hydrated) {
149168
if (noWrapper) {
150169
return children;
151170
}
152171
return (
153-
<div ref={childRef} style={{ display: "contents" }} {...rest}>
172+
<WrapperElement ref={childRef} style={{ display: "contents" }} {...rest}>
154173
{children}
155-
</div>
174+
</WrapperElement>
156175
);
157176
} else {
158177
return (
159-
<div
178+
<WrapperElement
179+
{...rest}
160180
ref={childRef}
161-
style={{ display: "contents" }}
162181
suppressHydrationWarning
163-
{...rest}
164182
dangerouslySetInnerHTML={{ __html: "" }}
165183
/>
166184
);

0 commit comments

Comments
 (0)