Shared useinview

This commit is contained in:
2024-09-27 11:01:36 +05:30
parent 127e024374
commit d64da34b1e
7 changed files with 221 additions and 135 deletions

View 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);
};

View File

@ -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;

View 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;