use in viewport

This commit is contained in:
Nate Kelley 2025-03-14 12:43:30 -06:00
parent 0fde170eee
commit d898e7ecc0
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 172 additions and 0 deletions

View File

@ -0,0 +1,64 @@
import { DependencyList, EffectCallback, useEffect, useLayoutEffect } from 'react';
import { useRef } from 'react';
import { useUnmount } from './useUnmount';
import { depsAreSame } from '@/lib/depAreSame';
import type { BasicTarget } from '@/lib/domTarget';
import { getTargetElement } from '@/lib/domTarget';
const createEffectWithTarget = (useEffectType: typeof useEffect | typeof useLayoutEffect) => {
/**
*
* @param effect
* @param deps
* @param target target should compare ref.current vs ref.current, dom vs dom, ()=>dom vs ()=>dom
*/
const useEffectWithTarget = (
effect: EffectCallback,
deps: DependencyList,
target: BasicTarget<any> | BasicTarget<any>[]
) => {
const hasInitRef = useRef(false);
const lastElementRef = useRef<(Element | null)[]>([]);
const lastDepsRef = useRef<DependencyList>([]);
const unLoadRef = useRef<any>();
useEffectType(() => {
const targets = Array.isArray(target) ? target : [target];
const els = targets.map((item) => getTargetElement(item));
// init run
if (!hasInitRef.current) {
hasInitRef.current = true;
lastElementRef.current = els;
lastDepsRef.current = deps;
unLoadRef.current = effect();
return;
}
if (
els.length !== lastElementRef.current.length ||
!depsAreSame(lastElementRef.current, els) ||
!depsAreSame(lastDepsRef.current, deps)
) {
unLoadRef.current?.();
lastElementRef.current = els;
lastDepsRef.current = deps;
unLoadRef.current = effect();
}
});
useUnmount(() => {
unLoadRef.current?.();
// for react-refresh
hasInitRef.current = false;
});
};
return useEffectWithTarget;
};
export const useEffectWithTarget = createEffectWithTarget(useEffect);

View File

@ -0,0 +1,58 @@
import 'intersection-observer';
import { useState } from 'react';
import type { BasicTarget } from '../lib/domTarget';
import { getTargetElement } from '../lib/domTarget';
import { useEffectWithTarget } from './useEffectWithTarget';
type CallbackType = (entry: IntersectionObserverEntry) => void;
export interface Options {
rootMargin?: string;
threshold?: number | number[];
root?: BasicTarget<Element>;
callback?: CallbackType;
}
function useInViewport(target: BasicTarget | BasicTarget[], options?: Options) {
const { callback, ...option } = options || {};
const [state, setState] = useState<boolean>();
const [ratio, setRatio] = useState<number>();
useEffectWithTarget(
() => {
const targets = Array.isArray(target) ? target : [target];
const els = targets.map((element) => getTargetElement(element)).filter(Boolean);
if (!els.length) {
return;
}
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
setRatio(entry.intersectionRatio);
setState(entry.isIntersecting);
callback?.(entry);
}
},
{
...option,
root: getTargetElement(options?.root)
}
);
els.forEach((el) => observer.observe(el!));
return () => {
observer.disconnect();
};
},
[options?.rootMargin, options?.threshold, callback],
target
);
return [state, ratio] as const;
}
export default useInViewport;

View File

@ -0,0 +1,9 @@
import type { DependencyList } from 'react';
export function depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean {
if (oldDeps === deps) return true;
for (let i = 0; i < oldDeps.length; i++) {
if (!Object.is(oldDeps[i], deps[i])) return false;
}
return true;
}

34
web/src/lib/domTarget.ts Normal file
View File

@ -0,0 +1,34 @@
import type { MutableRefObject } from 'react';
import isFunction from 'lodash/isFunction';
import isBrowser from './isBrowser';
type TargetValue<T> = T | undefined | null;
type TargetType = HTMLElement | Element | Window | Document;
export type BasicTarget<T extends TargetType = Element> =
| (() => TargetValue<T>)
| TargetValue<T>
| MutableRefObject<TargetValue<T>>;
export function getTargetElement<T extends TargetType>(target: BasicTarget<T>, defaultElement?: T) {
if (!isBrowser) {
return undefined;
}
if (!target) {
return defaultElement;
}
let targetElement: TargetValue<T>;
if (isFunction(target)) {
targetElement = target();
} else if ('current' in target) {
targetElement = target.current;
} else {
targetElement = target;
}
return targetElement;
}

7
web/src/lib/isBrowser.ts Normal file
View File

@ -0,0 +1,7 @@
export const isBrowser = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
export default isBrowser;