Shared useinview
This commit is contained in:
80
src/hooks/shared-intersection-observer.tsx
Normal file
80
src/hooks/shared-intersection-observer.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { useEffect } from 'react';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
type Callback = (entry: IntersectionObserverEntry) => void;
|
||||
|
||||
class IntersectionObserverManager {
|
||||
private observer: IntersectionObserver;
|
||||
private callbacks: Map<Element, Callback>;
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
constructor(options: IntersectionObserverInit) {
|
||||
this.callbacks = new Map();
|
||||
|
||||
this.observer = new IntersectionObserver(
|
||||
this.handleIntersect.bind(this),
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
private handleIntersect(entries: IntersectionObserverEntry[]) {
|
||||
entries.forEach((entry) => {
|
||||
const callback = this.callbacks.get(entry.target);
|
||||
if (callback) {
|
||||
callback(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
observe(element: Element, callback: Callback) {
|
||||
if (element && callback) {
|
||||
this.callbacks.set(element, callback);
|
||||
this.observer.observe(element);
|
||||
}
|
||||
}
|
||||
|
||||
unobserve(element: Element) {
|
||||
if (element) {
|
||||
this.callbacks.delete(element);
|
||||
this.observer.unobserve(element);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.observer.disconnect();
|
||||
this.callbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
let manager: IntersectionObserverManager | null = null;
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const getObserverManager = (options: IntersectionObserverInit) => {
|
||||
if (!manager) {
|
||||
manager = new IntersectionObserverManager(options);
|
||||
}
|
||||
return manager;
|
||||
};
|
||||
|
||||
export const useSharedIntersectionObserver = (
|
||||
// eslint-disable-next-line no-undef
|
||||
options: IntersectionObserverInit,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Cleanup logic if needed
|
||||
// For example, disconnect the observer when the component unmounts entirely
|
||||
// This depends on your application's lifecycle
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
// Return a dummy manager for SSR
|
||||
return {
|
||||
observe: () => {},
|
||||
unobserve: () => {},
|
||||
disconnect: () => {},
|
||||
} as unknown as IntersectionObserverManager;
|
||||
}
|
||||
|
||||
return getObserverManager(options);
|
||||
};
|
||||
@ -1,63 +1,50 @@
|
||||
import { useState, useEffect, RefObject } from 'react';
|
||||
'use client';
|
||||
import { useState, useEffect, useRef, RefObject } from 'react';
|
||||
import { useSharedIntersectionObserver } from './shared-intersection-observer';
|
||||
|
||||
interface UseInViewOptions {
|
||||
threshold?: number | number[];
|
||||
// eslint-disable-next-line no-undef
|
||||
interface UseInViewOptions extends IntersectionObserverInit {
|
||||
triggerOnce?: boolean;
|
||||
}
|
||||
|
||||
const useInView = (
|
||||
ref: RefObject<Element>,
|
||||
options: UseInViewOptions = { threshold: 0.1, triggerOnce: true },
|
||||
): boolean => {
|
||||
const { threshold = 0.1, triggerOnce = true } = options;
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
|
||||
// Validate threshold
|
||||
const isValidThreshold =
|
||||
typeof threshold === 'number'
|
||||
? threshold >= 0 && threshold <= 1
|
||||
: Array.isArray(threshold) && threshold.every((t) => t >= 0 && t <= 1);
|
||||
|
||||
if (!isValidThreshold) {
|
||||
console.warn(
|
||||
'Invalid threshold value passed to useInView. It should be between 0 and 1.',
|
||||
);
|
||||
}
|
||||
|
||||
options: UseInViewOptions,
|
||||
): [RefObject<HTMLImageElement>, boolean] => {
|
||||
const [isInView, setIsInView] = useState<boolean>(false);
|
||||
const elementRef = useRef<HTMLImageElement>(null);
|
||||
const observerManager = useSharedIntersectionObserver(options);
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const prefersReducedMotion = window.matchMedia(
|
||||
'(prefers-reduced-motion: reduce)',
|
||||
);
|
||||
if (prefersReducedMotion.matches) {
|
||||
setIsVisible(true);
|
||||
setIsInView(true);
|
||||
if (options.triggerOnce) {
|
||||
observerManager.unobserve(element);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
if (triggerOnce) {
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold },
|
||||
);
|
||||
const callback = (entry: IntersectionObserverEntry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsInView(true);
|
||||
if (options.triggerOnce) {
|
||||
observerManager.unobserve(element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
observer.observe(element);
|
||||
observerManager.observe(element, callback);
|
||||
|
||||
return () => {
|
||||
if (element) observer.unobserve(element);
|
||||
observerManager.unobserve(element);
|
||||
};
|
||||
}, [ref, threshold, triggerOnce]);
|
||||
}, [observerManager]);
|
||||
|
||||
return isVisible;
|
||||
return [elementRef, isInView];
|
||||
};
|
||||
|
||||
export default useInView;
|
||||
|
||||
63
src/hooks/useInViewsingle.tsx
Normal file
63
src/hooks/useInViewsingle.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { useState, useEffect, RefObject } from 'react';
|
||||
|
||||
interface UseInViewOptions {
|
||||
threshold?: number | number[];
|
||||
triggerOnce?: boolean;
|
||||
}
|
||||
|
||||
const useInView = (
|
||||
ref: RefObject<Element>,
|
||||
options: UseInViewOptions = { threshold: 0.1, triggerOnce: true },
|
||||
): boolean => {
|
||||
const { threshold = 0.1, triggerOnce = true } = options;
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
|
||||
// Validate threshold
|
||||
const isValidThreshold =
|
||||
typeof threshold === 'number'
|
||||
? threshold >= 0 && threshold <= 1
|
||||
: Array.isArray(threshold) && threshold.every((t) => t >= 0 && t <= 1);
|
||||
|
||||
if (!isValidThreshold) {
|
||||
console.warn(
|
||||
'Invalid threshold value passed to useInView. It should be between 0 and 1.',
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
const prefersReducedMotion = window.matchMedia(
|
||||
'(prefers-reduced-motion: reduce)',
|
||||
);
|
||||
if (prefersReducedMotion.matches) {
|
||||
setIsVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
if (triggerOnce) {
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold },
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
|
||||
return () => {
|
||||
if (element) observer.unobserve(element);
|
||||
};
|
||||
}, [ref, threshold, triggerOnce]);
|
||||
|
||||
return isVisible;
|
||||
};
|
||||
|
||||
export default useInView;
|
||||
Reference in New Issue
Block a user