Skip to content

Commit

Permalink
useIntersectionObserver
Browse files Browse the repository at this point in the history
  • Loading branch information
typeofweb committed Jan 8, 2021
1 parent e2198e7 commit 6f30e73
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 1 deletion.
13 changes: 12 additions & 1 deletion components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { CSSProperties } from 'react';
import { useEffect } from 'react';

import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';

import styles from './table.module.scss';
import { useFloatingTableHeader } from './useFloatingTableHeader';
Expand Down Expand Up @@ -28,16 +31,24 @@ const TableHeaderRow = ({
);
};

const intersectionObserverOptions = {};

export const Table = <T extends { readonly id: string }>({ data, columns }: TableProps<T>) => {
const { tableRef, floatingHeaderRef } = useFloatingTableHeader<HTMLDivElement>();

const { setRef, entry } = useIntersectionObserver<HTMLTableSectionElement>(
intersectionObserverOptions,
);

console.log({ entry });

return (
<div className={styles.tableWrapper}>
<div ref={floatingHeaderRef} aria-hidden={true}>
<TableHeaderRow columns={columns} as="span" />
</div>
<table ref={tableRef} className={styles.table}>
<thead>
<thead ref={setRef}>
<tr>
<TableHeaderRow columns={columns} as="th" />
</tr>
Expand Down
100 changes: 100 additions & 0 deletions hooks/useIntersectionObserver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useCallback, useEffect, useRef, useState } from 'react';

import { useWillUnmount } from './useWillUnmount';

type UseIntersectionObserverArgs = Pick<IntersectionObserverInit, 'rootMargin' | 'threshold'>;
type ObserverCallback = (entry: IntersectionObserverEntry) => void;
type Observer = {
readonly key: string;
readonly intersectionObserver: IntersectionObserver;
// eslint-disable-next-line
readonly elementToCallback: Map<Element, ObserverCallback>;
};

export const useIntersectionObserver = <T extends Element = HTMLElement>(
options: UseIntersectionObserverArgs,
) => {
const unobserve = useRef<() => void>();

const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);

const setRef = useCallback(
(el: T) => {
console.log({ el });

if (unobserve.current) {

This comment has been minimized.

Copy link
@wisnie

wisnie Jan 8, 2021

Member

Ten kod pojawia się w takiej samej postaci w dwóch miejscach, jakaś prosta abstrakcja by tutaj pasowała.

unobserve.current();
unobserve.current = undefined;
}

if (el && el.tagName) {
unobserve.current = observe(el, setEntry, options);
}
},
[options],
);

useWillUnmount(() => {
if (unobserve.current) {
unobserve.current();
unobserve.current = undefined;
}
});

if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') {
return { setRef: () => {}, entry: null } as const;
}

return { setRef, entry } as const;
};

const observe = (() => {
const observers = new Map<string, Observer>();

const createObserver = (options: UseIntersectionObserverArgs) => {
const key = JSON.stringify(options);
if (observers.has(key)) {
return observers.get(key)!;
}

const elementToCallback = new Map<Element, ObserverCallback>();
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const callback = elementToCallback.get(entry.target);
if (callback) {
callback(entry);
}
});
}, options);

const observer: Observer = {
key,
elementToCallback,
intersectionObserver,
};
observers.set(key, observer);

return observer;
};

return <T extends Element>(
el: T,
callback: ObserverCallback,
options: UseIntersectionObserverArgs,
) => {
const { key, elementToCallback, intersectionObserver } = createObserver(options);
elementToCallback.set(el, callback);
intersectionObserver.observe(el);

const unobserve = () => {
intersectionObserver.unobserve(el);
elementToCallback.delete(el);

if (elementToCallback.size === 0) {
intersectionObserver.disconnect();
observers.delete(key);
}
};
return unobserve;
};
})();
7 changes: 7 additions & 0 deletions hooks/useWillUnmount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { EffectCallback } from 'react';
import { useEffect } from 'react';

/* eslint-disable react-hooks/exhaustive-deps */
export const useWillUnmount = (cb: ReturnType<EffectCallback>) => {
useEffect(() => cb, []);
};

0 comments on commit 6f30e73

Please sign in to comment.