Skip to content

Commit 6f30e73

Browse files
committed
useIntersectionObserver
1 parent e2198e7 commit 6f30e73

File tree

3 files changed

+119
-1
lines changed

3 files changed

+119
-1
lines changed

components/Table/Table.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import type { CSSProperties } from 'react';
2+
import { useEffect } from 'react';
3+
4+
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
25

36
import styles from './table.module.scss';
47
import { useFloatingTableHeader } from './useFloatingTableHeader';
@@ -28,16 +31,24 @@ const TableHeaderRow = ({
2831
);
2932
};
3033

34+
const intersectionObserverOptions = {};
35+
3136
export const Table = <T extends { readonly id: string }>({ data, columns }: TableProps<T>) => {
3237
const { tableRef, floatingHeaderRef } = useFloatingTableHeader<HTMLDivElement>();
3338

39+
const { setRef, entry } = useIntersectionObserver<HTMLTableSectionElement>(
40+
intersectionObserverOptions,
41+
);
42+
43+
console.log({ entry });
44+
3445
return (
3546
<div className={styles.tableWrapper}>
3647
<div ref={floatingHeaderRef} aria-hidden={true}>
3748
<TableHeaderRow columns={columns} as="span" />
3849
</div>
3950
<table ref={tableRef} className={styles.table}>
40-
<thead>
51+
<thead ref={setRef}>
4152
<tr>
4253
<TableHeaderRow columns={columns} as="th" />
4354
</tr>

hooks/useIntersectionObserver.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useCallback, useEffect, useRef, useState } from 'react';
2+
3+
import { useWillUnmount } from './useWillUnmount';
4+
5+
type UseIntersectionObserverArgs = Pick<IntersectionObserverInit, 'rootMargin' | 'threshold'>;
6+
type ObserverCallback = (entry: IntersectionObserverEntry) => void;
7+
type Observer = {
8+
readonly key: string;
9+
readonly intersectionObserver: IntersectionObserver;
10+
// eslint-disable-next-line
11+
readonly elementToCallback: Map<Element, ObserverCallback>;
12+
};
13+
14+
export const useIntersectionObserver = <T extends Element = HTMLElement>(
15+
options: UseIntersectionObserverArgs,
16+
) => {
17+
const unobserve = useRef<() => void>();
18+
19+
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
20+
21+
const setRef = useCallback(
22+
(el: T) => {
23+
console.log({ el });
24+
25+
if (unobserve.current) {
26+
unobserve.current();
27+
unobserve.current = undefined;
28+
}
29+
30+
if (el && el.tagName) {
31+
unobserve.current = observe(el, setEntry, options);
32+
}
33+
},
34+
[options],
35+
);
36+
37+
useWillUnmount(() => {
38+
if (unobserve.current) {
39+
unobserve.current();
40+
unobserve.current = undefined;
41+
}
42+
});
43+
44+
if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') {
45+
return { setRef: () => {}, entry: null } as const;
46+
}
47+
48+
return { setRef, entry } as const;
49+
};
50+
51+
const observe = (() => {
52+
const observers = new Map<string, Observer>();
53+
54+
const createObserver = (options: UseIntersectionObserverArgs) => {
55+
const key = JSON.stringify(options);
56+
if (observers.has(key)) {
57+
return observers.get(key)!;
58+
}
59+
60+
const elementToCallback = new Map<Element, ObserverCallback>();
61+
const intersectionObserver = new IntersectionObserver((entries) => {
62+
entries.forEach((entry) => {
63+
const callback = elementToCallback.get(entry.target);
64+
if (callback) {
65+
callback(entry);
66+
}
67+
});
68+
}, options);
69+
70+
const observer: Observer = {
71+
key,
72+
elementToCallback,
73+
intersectionObserver,
74+
};
75+
observers.set(key, observer);
76+
77+
return observer;
78+
};
79+
80+
return <T extends Element>(
81+
el: T,
82+
callback: ObserverCallback,
83+
options: UseIntersectionObserverArgs,
84+
) => {
85+
const { key, elementToCallback, intersectionObserver } = createObserver(options);
86+
elementToCallback.set(el, callback);
87+
intersectionObserver.observe(el);
88+
89+
const unobserve = () => {
90+
intersectionObserver.unobserve(el);
91+
elementToCallback.delete(el);
92+
93+
if (elementToCallback.size === 0) {
94+
intersectionObserver.disconnect();
95+
observers.delete(key);
96+
}
97+
};
98+
return unobserve;
99+
};
100+
})();

hooks/useWillUnmount.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { EffectCallback } from 'react';
2+
import { useEffect } from 'react';
3+
4+
/* eslint-disable react-hooks/exhaustive-deps */
5+
export const useWillUnmount = (cb: ReturnType<EffectCallback>) => {
6+
useEffect(() => cb, []);
7+
};

0 commit comments

Comments
 (0)