Last active: a year ago
import { useEffect, useRef } from 'react';
/**
* 利用 IntersectionObserver 实现滚动加载新数据的 hooks
*
* @param intersectedCallback 对应 anchor 相交时执行的回调
* @param option 判断条件,任意字段为 truthy 时不执行回调
*
* option 的变化才会使得重新 observe target
*
* @usage
* ```ts
* const { ref } = useScrollLoading(
* () => {
* setPage((d) => {
* d.page++;
* });
* },
* {
* loading,
* len: data?.data.length === 0,
* end: data?.last_page === data?.current_page,
* }
* );
* ```
*
* 通常情况下可以直接传递引用值的 option,
*
* 如果列表过长
* React 需要很长的时间来渲染,
*
* 则需要更精细的控制 option。
*
* 否则会导致在 React 列表未渲染完成的情况下触发
* observe 回调
*
* @usage
* ```ts
* const [condition, setCondition] = useState({
* loading: loading || folderLoading,
* string: typeof data === 'string',
* len: typeof data !== 'string' && data?.current_page === data?.last_page,
* hasData: !list.length,
* });
* // 等待素材缩略图(列表渲染)加载完成再更新 option
* const handleLoad = useCallback(() => {
* setCondition({
* loading: loading || folderLoading,
* string: typeof data === 'string',
* len: typeof data !== 'string' && data?.current_page === data?.last_page,
* hasData: !list.length,
* });
* }, [data, folderLoading, list.length, loading, setCondition]);
* const { ref } = useScrollLoading(() => {
* setPage((d) => {
* d.page++;
* });
* }, condition);
* ```
*
* 除此之外,在使用 Suspense 时,锚点必须在 Suspense 内且 fallback
*
* 参数不能为 boolean 值
*
* ```tsx
* <Suspense fallback={<></>}>
* 素材
* {list.map((a) => (
* <AssetItem key={a.id} asset={a} onLoad={handleLoad} />
* ))}
* 滚动加载锚点
* <div style={{ height: 1 }} ref={ref}></div>
* </Suspense>
* ```
*/
const useScrollLoading = (
intersectedCallback: () => void,
option?: {
[key in string]: boolean;
}
) => {
// Anchor ref
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const callback: IntersectionObserverCallback = (entries, observer) => {
const condition = option ?? {};
entries.forEach((entry) => {
for (const key of Object.keys(condition)) {
if (condition[key]) {
return;
}
}
if (entry.isIntersecting) {
intersectedCallback();
ref.current && observer.unobserve(ref.current);
}
});
};
const observe = new IntersectionObserver(callback);
if (!ref.current) {
console.error('Cannot get target ref');
return;
}
observe.observe(ref.current);
const target = ref.current;
return () => {
observe.unobserve(target);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [option]);
return {
ref,
};
};
export default useScrollLoading;