@usefy/use-intersection-observer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createInitialEntry: () => createInitialEntry,
24
+ isIntersectionObserverSupported: () => isIntersectionObserverSupported,
25
+ toIntersectionEntry: () => toIntersectionEntry,
26
+ useIntersectionObserver: () => useIntersectionObserver
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/useIntersectionObserver.ts
31
+ var import_react = require("react");
32
+
33
+ // src/utils.ts
34
+ function isIntersectionObserverSupported() {
35
+ return typeof window !== "undefined" && "IntersectionObserver" in window;
36
+ }
37
+ function toIntersectionEntry(nativeEntry) {
38
+ return {
39
+ entry: nativeEntry,
40
+ isIntersecting: nativeEntry.isIntersecting,
41
+ intersectionRatio: nativeEntry.intersectionRatio,
42
+ target: nativeEntry.target,
43
+ boundingClientRect: nativeEntry.boundingClientRect,
44
+ intersectionRect: nativeEntry.intersectionRect,
45
+ rootBounds: nativeEntry.rootBounds,
46
+ time: nativeEntry.time
47
+ };
48
+ }
49
+ function createInitialEntry(isIntersecting, target = null) {
50
+ if (!isIntersecting) {
51
+ return null;
52
+ }
53
+ const emptyRect = {
54
+ x: 0,
55
+ y: 0,
56
+ width: 0,
57
+ height: 0,
58
+ top: 0,
59
+ right: 0,
60
+ bottom: 0,
61
+ left: 0,
62
+ toJSON: () => ({})
63
+ };
64
+ const mockNativeEntry = {
65
+ target,
66
+ isIntersecting,
67
+ intersectionRatio: isIntersecting ? 1 : 0,
68
+ boundingClientRect: emptyRect,
69
+ intersectionRect: emptyRect,
70
+ rootBounds: null,
71
+ time: typeof performance !== "undefined" ? performance.now() : Date.now()
72
+ };
73
+ return {
74
+ entry: mockNativeEntry,
75
+ isIntersecting,
76
+ intersectionRatio: isIntersecting ? 1 : 0,
77
+ target,
78
+ boundingClientRect: emptyRect,
79
+ intersectionRect: emptyRect,
80
+ rootBounds: null,
81
+ time: mockNativeEntry.time
82
+ };
83
+ }
84
+ function createNoopRef() {
85
+ return () => {
86
+ };
87
+ }
88
+
89
+ // src/useIntersectionObserver.ts
90
+ function useIntersectionObserver(options = {}) {
91
+ const {
92
+ threshold = 0,
93
+ root = null,
94
+ rootMargin = "0px",
95
+ triggerOnce = false,
96
+ enabled = true,
97
+ initialIsIntersecting = false,
98
+ onChange,
99
+ delay = 0
100
+ } = options;
101
+ const isSupported = isIntersectionObserverSupported();
102
+ const observerRef = (0, import_react.useRef)(null);
103
+ const targetRef = (0, import_react.useRef)(null);
104
+ const hasTriggeredRef = (0, import_react.useRef)(false);
105
+ const delayTimeoutRef = (0, import_react.useRef)(null);
106
+ const prevEntryRef = (0, import_react.useRef)(null);
107
+ const onChangeRef = (0, import_react.useRef)(onChange);
108
+ onChangeRef.current = onChange;
109
+ const [, forceUpdate] = (0, import_react.useState)({});
110
+ const [entry, setEntry] = (0, import_react.useState)(
111
+ () => initialIsIntersecting ? createInitialEntry(true, null) : null
112
+ );
113
+ const inView = entry?.isIntersecting ?? initialIsIntersecting;
114
+ const handleIntersection = (0, import_react.useCallback)(
115
+ (entries) => {
116
+ entries.forEach((nativeEntry) => {
117
+ const intersectionEntry = toIntersectionEntry(nativeEntry);
118
+ const prevEntry = prevEntryRef.current;
119
+ const hasChanged = !prevEntry || prevEntry.isIntersecting !== nativeEntry.isIntersecting || prevEntry.intersectionRatio !== nativeEntry.intersectionRatio;
120
+ prevEntryRef.current = {
121
+ isIntersecting: nativeEntry.isIntersecting,
122
+ intersectionRatio: nativeEntry.intersectionRatio
123
+ };
124
+ if (hasChanged) {
125
+ setEntry(intersectionEntry);
126
+ }
127
+ if (onChangeRef.current) {
128
+ onChangeRef.current(intersectionEntry, nativeEntry.isIntersecting);
129
+ }
130
+ if (triggerOnce && nativeEntry.isIntersecting) {
131
+ hasTriggeredRef.current = true;
132
+ if (observerRef.current && nativeEntry.target) {
133
+ observerRef.current.unobserve(nativeEntry.target);
134
+ }
135
+ }
136
+ });
137
+ },
138
+ [triggerOnce]
139
+ );
140
+ const setRef = (0, import_react.useCallback)((node) => {
141
+ const prevTarget = targetRef.current;
142
+ targetRef.current = node;
143
+ if (prevTarget !== node) {
144
+ forceUpdate({});
145
+ }
146
+ }, []);
147
+ (0, import_react.useEffect)(() => {
148
+ if (!isSupported) {
149
+ return;
150
+ }
151
+ if (observerRef.current) {
152
+ observerRef.current.disconnect();
153
+ observerRef.current = null;
154
+ }
155
+ if (!enabled) {
156
+ return;
157
+ }
158
+ if (triggerOnce && hasTriggeredRef.current) {
159
+ return;
160
+ }
161
+ if (!targetRef.current) {
162
+ return;
163
+ }
164
+ const target = targetRef.current;
165
+ const createAndObserve = () => {
166
+ observerRef.current = new IntersectionObserver(handleIntersection, {
167
+ threshold,
168
+ root,
169
+ rootMargin
170
+ });
171
+ if (target) {
172
+ observerRef.current.observe(target);
173
+ }
174
+ };
175
+ if (delay > 0) {
176
+ delayTimeoutRef.current = setTimeout(() => {
177
+ createAndObserve();
178
+ delayTimeoutRef.current = null;
179
+ }, delay);
180
+ return () => {
181
+ if (delayTimeoutRef.current) {
182
+ clearTimeout(delayTimeoutRef.current);
183
+ delayTimeoutRef.current = null;
184
+ }
185
+ if (observerRef.current) {
186
+ observerRef.current.disconnect();
187
+ observerRef.current = null;
188
+ }
189
+ };
190
+ }
191
+ createAndObserve();
192
+ return () => {
193
+ if (observerRef.current) {
194
+ observerRef.current.disconnect();
195
+ observerRef.current = null;
196
+ }
197
+ };
198
+ }, [
199
+ enabled,
200
+ triggerOnce,
201
+ delay,
202
+ threshold,
203
+ root,
204
+ rootMargin,
205
+ handleIntersection,
206
+ isSupported,
207
+ // Include target change trigger
208
+ targetRef.current
209
+ ]);
210
+ (0, import_react.useEffect)(() => {
211
+ if (!triggerOnce) {
212
+ hasTriggeredRef.current = false;
213
+ }
214
+ }, [triggerOnce]);
215
+ if (!isSupported) {
216
+ return {
217
+ entry: initialIsIntersecting ? createInitialEntry(true, null) : null,
218
+ inView: initialIsIntersecting,
219
+ ref: createNoopRef()
220
+ };
221
+ }
222
+ return {
223
+ entry,
224
+ inView,
225
+ ref: setRef
226
+ };
227
+ }
228
+ // Annotate the CommonJS export names for ESM import in node:
229
+ 0 && (module.exports = {
230
+ createInitialEntry,
231
+ isIntersectionObserverSupported,
232
+ toIntersectionEntry,
233
+ useIntersectionObserver
234
+ });
235
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/useIntersectionObserver.ts","../src/utils.ts"],"sourcesContent":["export { useIntersectionObserver } from \"./useIntersectionObserver\";\nexport type {\n UseIntersectionObserverOptions,\n UseIntersectionObserverReturn,\n IntersectionEntry,\n OnChangeCallback,\n} from \"./types\";\nexport {\n isIntersectionObserverSupported,\n toIntersectionEntry,\n createInitialEntry,\n} from \"./utils\";\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type {\n UseIntersectionObserverOptions,\n UseIntersectionObserverReturn,\n IntersectionEntry,\n OnChangeCallback,\n} from \"./types\";\nimport {\n isIntersectionObserverSupported,\n toIntersectionEntry,\n createInitialEntry,\n createNoopRef,\n} from \"./utils\";\n\n/**\n * A React hook for observing element visibility using the Intersection Observer API.\n *\n * Features:\n * - Efficient viewport/container visibility detection\n * - Threshold-based intersection callbacks\n * - TriggerOnce support for lazy loading patterns\n * - Dynamic enable/disable support\n * - SSR compatible with graceful degradation\n * - TypeScript support with full type inference\n *\n * @param options - Configuration options for the observer\n * @returns Object containing entry data, inView boolean, and ref callback\n *\n * @example\n * ```tsx\n * // Basic usage - check if element is visible\n * function Component() {\n * const { ref, inView } = useIntersectionObserver();\n * return (\n * <div ref={ref}>\n * {inView ? 'Visible!' : 'Not visible'}\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Lazy load image when it enters viewport\n * function LazyImage({ src, alt }: { src: string; alt: string }) {\n * const { ref, inView } = useIntersectionObserver({\n * triggerOnce: true,\n * threshold: 0.1,\n * });\n *\n * return (\n * <div ref={ref}>\n * {inView ? (\n * <img src={src} alt={alt} />\n * ) : (\n * <div className=\"placeholder\" />\n * )}\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Infinite scroll with sentinel element\n * function InfiniteList() {\n * const { ref, inView } = useIntersectionObserver({\n * threshold: 1.0,\n * rootMargin: '100px',\n * });\n *\n * useEffect(() => {\n * if (inView) {\n * loadMoreItems();\n * }\n * }, [inView]);\n *\n * return (\n * <div>\n * {items.map(item => <Item key={item.id} {...item} />)}\n * <div ref={ref} />\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Track scroll progress with multiple thresholds\n * function ProgressTracker() {\n * const { ref, entry } = useIntersectionObserver({\n * threshold: [0, 0.25, 0.5, 0.75, 1.0],\n * onChange: (entry, inView) => {\n * console.log('Progress:', Math.round(entry.intersectionRatio * 100), '%');\n * },\n * });\n *\n * return <div ref={ref}>Long content...</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Custom scroll container as root\n * function ScrollContainer() {\n * const containerRef = useRef<HTMLDivElement>(null);\n * const { ref, inView } = useIntersectionObserver({\n * root: containerRef.current,\n * rootMargin: '0px',\n * });\n *\n * return (\n * <div ref={containerRef} style={{ overflow: 'auto', height: 400 }}>\n * <div style={{ height: 1000 }}>\n * <div ref={ref}>{inView ? 'In container view' : 'Outside'}</div>\n * </div>\n * </div>\n * );\n * }\n * ```\n */\nexport function useIntersectionObserver(\n options: UseIntersectionObserverOptions = {}\n): UseIntersectionObserverReturn {\n const {\n threshold = 0,\n root = null,\n rootMargin = \"0px\",\n triggerOnce = false,\n enabled = true,\n initialIsIntersecting = false,\n onChange,\n delay = 0,\n } = options;\n\n // ============ SSR Check ============\n const isSupported = isIntersectionObserverSupported();\n\n // ============ Refs for internal state (no re-renders) ============\n const observerRef = useRef<IntersectionObserver | null>(null);\n const targetRef = useRef<Element | null>(null);\n const hasTriggeredRef = useRef<boolean>(false);\n const delayTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Store previous entry values for comparison (to avoid unnecessary re-renders)\n const prevEntryRef = useRef<{\n isIntersecting: boolean;\n intersectionRatio: number;\n } | null>(null);\n\n // Store callbacks in refs to avoid effect dependencies\n const onChangeRef = useRef<OnChangeCallback | undefined>(onChange);\n onChangeRef.current = onChange;\n\n // Force re-render trigger for ref changes\n const [, forceUpdate] = useState({});\n\n // ============ State (triggers re-renders) ============\n const [entry, setEntry] = useState<IntersectionEntry | null>(() =>\n initialIsIntersecting ? createInitialEntry(true, null) : null\n );\n\n // Compute inView from entry state\n const inView = entry?.isIntersecting ?? initialIsIntersecting;\n\n // ============ Observer Callback ============\n const handleIntersection = useCallback(\n (entries: IntersectionObserverEntry[]) => {\n entries.forEach((nativeEntry) => {\n const intersectionEntry = toIntersectionEntry(nativeEntry);\n\n // Check if meaningful values have changed (ignore time which always changes)\n const prevEntry = prevEntryRef.current;\n const hasChanged =\n !prevEntry ||\n prevEntry.isIntersecting !== nativeEntry.isIntersecting ||\n prevEntry.intersectionRatio !== nativeEntry.intersectionRatio;\n\n // Update previous entry ref\n prevEntryRef.current = {\n isIntersecting: nativeEntry.isIntersecting,\n intersectionRatio: nativeEntry.intersectionRatio,\n };\n\n // Only update state if meaningful values changed\n if (hasChanged) {\n setEntry(intersectionEntry);\n }\n\n // Call onChange callback if provided (always call, even if state didn't change)\n if (onChangeRef.current) {\n onChangeRef.current(intersectionEntry, nativeEntry.isIntersecting);\n }\n\n // Handle triggerOnce - unobserve after first intersection\n if (triggerOnce && nativeEntry.isIntersecting) {\n hasTriggeredRef.current = true;\n\n // Unobserve the target\n if (observerRef.current && nativeEntry.target) {\n observerRef.current.unobserve(nativeEntry.target);\n }\n }\n });\n },\n [triggerOnce]\n );\n\n // ============ Ref Callback ============\n const setRef = useCallback((node: Element | null) => {\n // Store previous target\n const prevTarget = targetRef.current;\n\n // Update target ref\n targetRef.current = node;\n\n // Force re-render to trigger useEffect with new target\n if (prevTarget !== node) {\n forceUpdate({});\n }\n }, []);\n\n // ============ Effect: Manage Observer Lifecycle ============\n useEffect(() => {\n // SSR guard\n if (!isSupported) {\n return;\n }\n\n // Clean up existing observer\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n\n // Don't create observer if disabled\n if (!enabled) {\n return;\n }\n\n // Don't create if triggerOnce already triggered\n if (triggerOnce && hasTriggeredRef.current) {\n return;\n }\n\n // Don't create if no target\n if (!targetRef.current) {\n return;\n }\n\n const target = targetRef.current;\n\n // Create and observe function\n const createAndObserve = () => {\n observerRef.current = new IntersectionObserver(handleIntersection, {\n threshold,\n root,\n rootMargin,\n });\n\n if (target) {\n observerRef.current.observe(target);\n }\n };\n\n // Handle delay\n if (delay > 0) {\n delayTimeoutRef.current = setTimeout(() => {\n createAndObserve();\n delayTimeoutRef.current = null;\n }, delay);\n\n return () => {\n if (delayTimeoutRef.current) {\n clearTimeout(delayTimeoutRef.current);\n delayTimeoutRef.current = null;\n }\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n }\n\n // Create observer immediately\n createAndObserve();\n\n // Cleanup\n return () => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n enabled,\n triggerOnce,\n delay,\n threshold,\n root,\n rootMargin,\n handleIntersection,\n isSupported,\n // Include target change trigger\n targetRef.current,\n ]);\n\n // ============ Effect: Reset hasTriggered when triggerOnce is disabled ============\n useEffect(() => {\n if (!triggerOnce) {\n hasTriggeredRef.current = false;\n }\n }, [triggerOnce]);\n\n // ============ SSR Return ============\n if (!isSupported) {\n return {\n entry: initialIsIntersecting ? createInitialEntry(true, null) : null,\n inView: initialIsIntersecting,\n ref: createNoopRef(),\n };\n }\n\n return {\n entry,\n inView,\n ref: setRef,\n };\n}\n","import type { IntersectionEntry } from \"./types\";\n\n/**\n * Check if IntersectionObserver API is supported in the current environment\n * Returns false in SSR environments or browsers without support\n */\nexport function isIntersectionObserverSupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n \"IntersectionObserver\" in window\n );\n}\n\n/**\n * Convert a native IntersectionObserverEntry to our IntersectionEntry type\n * Provides a consistent interface with additional convenience properties\n *\n * @param nativeEntry - The native IntersectionObserverEntry from the browser\n * @returns IntersectionEntry with all properties\n */\nexport function toIntersectionEntry(\n nativeEntry: IntersectionObserverEntry\n): IntersectionEntry {\n return {\n entry: nativeEntry,\n isIntersecting: nativeEntry.isIntersecting,\n intersectionRatio: nativeEntry.intersectionRatio,\n target: nativeEntry.target,\n boundingClientRect: nativeEntry.boundingClientRect,\n intersectionRect: nativeEntry.intersectionRect,\n rootBounds: nativeEntry.rootBounds,\n time: nativeEntry.time,\n };\n}\n\n/**\n * Create an initial IntersectionEntry for SSR or before first observation\n * Used when initialIsIntersecting is true\n *\n * @param isIntersecting - Whether to set initial state as intersecting\n * @param target - Optional target element (null for SSR)\n * @returns A mock IntersectionEntry\n */\nexport function createInitialEntry(\n isIntersecting: boolean,\n target: Element | null = null\n): IntersectionEntry | null {\n if (!isIntersecting) {\n return null;\n }\n\n // Create a placeholder DOMRect for SSR\n const emptyRect: DOMRectReadOnly = {\n x: 0,\n y: 0,\n width: 0,\n height: 0,\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n toJSON: () => ({}),\n };\n\n // Create a mock native entry\n const mockNativeEntry = {\n target: target as Element,\n isIntersecting,\n intersectionRatio: isIntersecting ? 1 : 0,\n boundingClientRect: emptyRect,\n intersectionRect: emptyRect,\n rootBounds: null,\n time: typeof performance !== \"undefined\" ? performance.now() : Date.now(),\n } as IntersectionObserverEntry;\n\n return {\n entry: mockNativeEntry,\n isIntersecting,\n intersectionRatio: isIntersecting ? 1 : 0,\n target: target as Element,\n boundingClientRect: emptyRect,\n intersectionRect: emptyRect,\n rootBounds: null,\n time: mockNativeEntry.time,\n };\n}\n\n/**\n * Normalize threshold to always be an array\n * Handles both single number and array inputs\n *\n * @param threshold - Single threshold or array of thresholds\n * @returns Array of threshold values\n */\nexport function normalizeThreshold(\n threshold: number | number[] | undefined\n): number[] {\n if (threshold === undefined) {\n return [0];\n }\n if (Array.isArray(threshold)) {\n return threshold;\n }\n return [threshold];\n}\n\n/**\n * Deep compare two IntersectionObserverInit options objects\n * Used to determine if observer needs to be recreated\n *\n * @param a - First options object\n * @param b - Second options object\n * @returns true if options are equal\n */\nexport function areOptionsEqual(\n a: IntersectionObserverInit,\n b: IntersectionObserverInit\n): boolean {\n // Compare root\n if (a.root !== b.root) {\n return false;\n }\n\n // Compare rootMargin\n if (a.rootMargin !== b.rootMargin) {\n return false;\n }\n\n // Compare threshold (normalize to arrays for comparison)\n const thresholdA = normalizeThreshold(a.threshold);\n const thresholdB = normalizeThreshold(b.threshold);\n\n if (thresholdA.length !== thresholdB.length) {\n return false;\n }\n\n for (let i = 0; i < thresholdA.length; i++) {\n if (thresholdA[i] !== thresholdB[i]) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Create a no-op ref callback for SSR environments\n * Returns a function that does nothing when called\n */\nexport function createNoopRef(): (node: Element | null) => void {\n return () => {\n // No-op for SSR\n };\n}\n\n/**\n * Validate rootMargin string format\n * rootMargin follows CSS margin syntax: \"10px\", \"10px 20px\", \"10px 20px 30px 40px\"\n *\n * @param rootMargin - The rootMargin string to validate\n * @returns true if valid format\n */\nexport function isValidRootMargin(rootMargin: string): boolean {\n // Basic validation - rootMargin should contain px or %\n // Browser will handle more detailed validation\n const pattern = /^(-?\\d+(\\.\\d+)?(px|%)?\\s*){1,4}$/;\n return pattern.test(rootMargin.trim());\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,205 @@
1
+ // src/useIntersectionObserver.ts
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ // src/utils.ts
5
+ function isIntersectionObserverSupported() {
6
+ return typeof window !== "undefined" && "IntersectionObserver" in window;
7
+ }
8
+ function toIntersectionEntry(nativeEntry) {
9
+ return {
10
+ entry: nativeEntry,
11
+ isIntersecting: nativeEntry.isIntersecting,
12
+ intersectionRatio: nativeEntry.intersectionRatio,
13
+ target: nativeEntry.target,
14
+ boundingClientRect: nativeEntry.boundingClientRect,
15
+ intersectionRect: nativeEntry.intersectionRect,
16
+ rootBounds: nativeEntry.rootBounds,
17
+ time: nativeEntry.time
18
+ };
19
+ }
20
+ function createInitialEntry(isIntersecting, target = null) {
21
+ if (!isIntersecting) {
22
+ return null;
23
+ }
24
+ const emptyRect = {
25
+ x: 0,
26
+ y: 0,
27
+ width: 0,
28
+ height: 0,
29
+ top: 0,
30
+ right: 0,
31
+ bottom: 0,
32
+ left: 0,
33
+ toJSON: () => ({})
34
+ };
35
+ const mockNativeEntry = {
36
+ target,
37
+ isIntersecting,
38
+ intersectionRatio: isIntersecting ? 1 : 0,
39
+ boundingClientRect: emptyRect,
40
+ intersectionRect: emptyRect,
41
+ rootBounds: null,
42
+ time: typeof performance !== "undefined" ? performance.now() : Date.now()
43
+ };
44
+ return {
45
+ entry: mockNativeEntry,
46
+ isIntersecting,
47
+ intersectionRatio: isIntersecting ? 1 : 0,
48
+ target,
49
+ boundingClientRect: emptyRect,
50
+ intersectionRect: emptyRect,
51
+ rootBounds: null,
52
+ time: mockNativeEntry.time
53
+ };
54
+ }
55
+ function createNoopRef() {
56
+ return () => {
57
+ };
58
+ }
59
+
60
+ // src/useIntersectionObserver.ts
61
+ function useIntersectionObserver(options = {}) {
62
+ const {
63
+ threshold = 0,
64
+ root = null,
65
+ rootMargin = "0px",
66
+ triggerOnce = false,
67
+ enabled = true,
68
+ initialIsIntersecting = false,
69
+ onChange,
70
+ delay = 0
71
+ } = options;
72
+ const isSupported = isIntersectionObserverSupported();
73
+ const observerRef = useRef(null);
74
+ const targetRef = useRef(null);
75
+ const hasTriggeredRef = useRef(false);
76
+ const delayTimeoutRef = useRef(null);
77
+ const prevEntryRef = useRef(null);
78
+ const onChangeRef = useRef(onChange);
79
+ onChangeRef.current = onChange;
80
+ const [, forceUpdate] = useState({});
81
+ const [entry, setEntry] = useState(
82
+ () => initialIsIntersecting ? createInitialEntry(true, null) : null
83
+ );
84
+ const inView = entry?.isIntersecting ?? initialIsIntersecting;
85
+ const handleIntersection = useCallback(
86
+ (entries) => {
87
+ entries.forEach((nativeEntry) => {
88
+ const intersectionEntry = toIntersectionEntry(nativeEntry);
89
+ const prevEntry = prevEntryRef.current;
90
+ const hasChanged = !prevEntry || prevEntry.isIntersecting !== nativeEntry.isIntersecting || prevEntry.intersectionRatio !== nativeEntry.intersectionRatio;
91
+ prevEntryRef.current = {
92
+ isIntersecting: nativeEntry.isIntersecting,
93
+ intersectionRatio: nativeEntry.intersectionRatio
94
+ };
95
+ if (hasChanged) {
96
+ setEntry(intersectionEntry);
97
+ }
98
+ if (onChangeRef.current) {
99
+ onChangeRef.current(intersectionEntry, nativeEntry.isIntersecting);
100
+ }
101
+ if (triggerOnce && nativeEntry.isIntersecting) {
102
+ hasTriggeredRef.current = true;
103
+ if (observerRef.current && nativeEntry.target) {
104
+ observerRef.current.unobserve(nativeEntry.target);
105
+ }
106
+ }
107
+ });
108
+ },
109
+ [triggerOnce]
110
+ );
111
+ const setRef = useCallback((node) => {
112
+ const prevTarget = targetRef.current;
113
+ targetRef.current = node;
114
+ if (prevTarget !== node) {
115
+ forceUpdate({});
116
+ }
117
+ }, []);
118
+ useEffect(() => {
119
+ if (!isSupported) {
120
+ return;
121
+ }
122
+ if (observerRef.current) {
123
+ observerRef.current.disconnect();
124
+ observerRef.current = null;
125
+ }
126
+ if (!enabled) {
127
+ return;
128
+ }
129
+ if (triggerOnce && hasTriggeredRef.current) {
130
+ return;
131
+ }
132
+ if (!targetRef.current) {
133
+ return;
134
+ }
135
+ const target = targetRef.current;
136
+ const createAndObserve = () => {
137
+ observerRef.current = new IntersectionObserver(handleIntersection, {
138
+ threshold,
139
+ root,
140
+ rootMargin
141
+ });
142
+ if (target) {
143
+ observerRef.current.observe(target);
144
+ }
145
+ };
146
+ if (delay > 0) {
147
+ delayTimeoutRef.current = setTimeout(() => {
148
+ createAndObserve();
149
+ delayTimeoutRef.current = null;
150
+ }, delay);
151
+ return () => {
152
+ if (delayTimeoutRef.current) {
153
+ clearTimeout(delayTimeoutRef.current);
154
+ delayTimeoutRef.current = null;
155
+ }
156
+ if (observerRef.current) {
157
+ observerRef.current.disconnect();
158
+ observerRef.current = null;
159
+ }
160
+ };
161
+ }
162
+ createAndObserve();
163
+ return () => {
164
+ if (observerRef.current) {
165
+ observerRef.current.disconnect();
166
+ observerRef.current = null;
167
+ }
168
+ };
169
+ }, [
170
+ enabled,
171
+ triggerOnce,
172
+ delay,
173
+ threshold,
174
+ root,
175
+ rootMargin,
176
+ handleIntersection,
177
+ isSupported,
178
+ // Include target change trigger
179
+ targetRef.current
180
+ ]);
181
+ useEffect(() => {
182
+ if (!triggerOnce) {
183
+ hasTriggeredRef.current = false;
184
+ }
185
+ }, [triggerOnce]);
186
+ if (!isSupported) {
187
+ return {
188
+ entry: initialIsIntersecting ? createInitialEntry(true, null) : null,
189
+ inView: initialIsIntersecting,
190
+ ref: createNoopRef()
191
+ };
192
+ }
193
+ return {
194
+ entry,
195
+ inView,
196
+ ref: setRef
197
+ };
198
+ }
199
+ export {
200
+ createInitialEntry,
201
+ isIntersectionObserverSupported,
202
+ toIntersectionEntry,
203
+ useIntersectionObserver
204
+ };
205
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useIntersectionObserver.ts","../src/utils.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type {\n UseIntersectionObserverOptions,\n UseIntersectionObserverReturn,\n IntersectionEntry,\n OnChangeCallback,\n} from \"./types\";\nimport {\n isIntersectionObserverSupported,\n toIntersectionEntry,\n createInitialEntry,\n createNoopRef,\n} from \"./utils\";\n\n/**\n * A React hook for observing element visibility using the Intersection Observer API.\n *\n * Features:\n * - Efficient viewport/container visibility detection\n * - Threshold-based intersection callbacks\n * - TriggerOnce support for lazy loading patterns\n * - Dynamic enable/disable support\n * - SSR compatible with graceful degradation\n * - TypeScript support with full type inference\n *\n * @param options - Configuration options for the observer\n * @returns Object containing entry data, inView boolean, and ref callback\n *\n * @example\n * ```tsx\n * // Basic usage - check if element is visible\n * function Component() {\n * const { ref, inView } = useIntersectionObserver();\n * return (\n * <div ref={ref}>\n * {inView ? 'Visible!' : 'Not visible'}\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Lazy load image when it enters viewport\n * function LazyImage({ src, alt }: { src: string; alt: string }) {\n * const { ref, inView } = useIntersectionObserver({\n * triggerOnce: true,\n * threshold: 0.1,\n * });\n *\n * return (\n * <div ref={ref}>\n * {inView ? (\n * <img src={src} alt={alt} />\n * ) : (\n * <div className=\"placeholder\" />\n * )}\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Infinite scroll with sentinel element\n * function InfiniteList() {\n * const { ref, inView } = useIntersectionObserver({\n * threshold: 1.0,\n * rootMargin: '100px',\n * });\n *\n * useEffect(() => {\n * if (inView) {\n * loadMoreItems();\n * }\n * }, [inView]);\n *\n * return (\n * <div>\n * {items.map(item => <Item key={item.id} {...item} />)}\n * <div ref={ref} />\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Track scroll progress with multiple thresholds\n * function ProgressTracker() {\n * const { ref, entry } = useIntersectionObserver({\n * threshold: [0, 0.25, 0.5, 0.75, 1.0],\n * onChange: (entry, inView) => {\n * console.log('Progress:', Math.round(entry.intersectionRatio * 100), '%');\n * },\n * });\n *\n * return <div ref={ref}>Long content...</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Custom scroll container as root\n * function ScrollContainer() {\n * const containerRef = useRef<HTMLDivElement>(null);\n * const { ref, inView } = useIntersectionObserver({\n * root: containerRef.current,\n * rootMargin: '0px',\n * });\n *\n * return (\n * <div ref={containerRef} style={{ overflow: 'auto', height: 400 }}>\n * <div style={{ height: 1000 }}>\n * <div ref={ref}>{inView ? 'In container view' : 'Outside'}</div>\n * </div>\n * </div>\n * );\n * }\n * ```\n */\nexport function useIntersectionObserver(\n options: UseIntersectionObserverOptions = {}\n): UseIntersectionObserverReturn {\n const {\n threshold = 0,\n root = null,\n rootMargin = \"0px\",\n triggerOnce = false,\n enabled = true,\n initialIsIntersecting = false,\n onChange,\n delay = 0,\n } = options;\n\n // ============ SSR Check ============\n const isSupported = isIntersectionObserverSupported();\n\n // ============ Refs for internal state (no re-renders) ============\n const observerRef = useRef<IntersectionObserver | null>(null);\n const targetRef = useRef<Element | null>(null);\n const hasTriggeredRef = useRef<boolean>(false);\n const delayTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Store previous entry values for comparison (to avoid unnecessary re-renders)\n const prevEntryRef = useRef<{\n isIntersecting: boolean;\n intersectionRatio: number;\n } | null>(null);\n\n // Store callbacks in refs to avoid effect dependencies\n const onChangeRef = useRef<OnChangeCallback | undefined>(onChange);\n onChangeRef.current = onChange;\n\n // Force re-render trigger for ref changes\n const [, forceUpdate] = useState({});\n\n // ============ State (triggers re-renders) ============\n const [entry, setEntry] = useState<IntersectionEntry | null>(() =>\n initialIsIntersecting ? createInitialEntry(true, null) : null\n );\n\n // Compute inView from entry state\n const inView = entry?.isIntersecting ?? initialIsIntersecting;\n\n // ============ Observer Callback ============\n const handleIntersection = useCallback(\n (entries: IntersectionObserverEntry[]) => {\n entries.forEach((nativeEntry) => {\n const intersectionEntry = toIntersectionEntry(nativeEntry);\n\n // Check if meaningful values have changed (ignore time which always changes)\n const prevEntry = prevEntryRef.current;\n const hasChanged =\n !prevEntry ||\n prevEntry.isIntersecting !== nativeEntry.isIntersecting ||\n prevEntry.intersectionRatio !== nativeEntry.intersectionRatio;\n\n // Update previous entry ref\n prevEntryRef.current = {\n isIntersecting: nativeEntry.isIntersecting,\n intersectionRatio: nativeEntry.intersectionRatio,\n };\n\n // Only update state if meaningful values changed\n if (hasChanged) {\n setEntry(intersectionEntry);\n }\n\n // Call onChange callback if provided (always call, even if state didn't change)\n if (onChangeRef.current) {\n onChangeRef.current(intersectionEntry, nativeEntry.isIntersecting);\n }\n\n // Handle triggerOnce - unobserve after first intersection\n if (triggerOnce && nativeEntry.isIntersecting) {\n hasTriggeredRef.current = true;\n\n // Unobserve the target\n if (observerRef.current && nativeEntry.target) {\n observerRef.current.unobserve(nativeEntry.target);\n }\n }\n });\n },\n [triggerOnce]\n );\n\n // ============ Ref Callback ============\n const setRef = useCallback((node: Element | null) => {\n // Store previous target\n const prevTarget = targetRef.current;\n\n // Update target ref\n targetRef.current = node;\n\n // Force re-render to trigger useEffect with new target\n if (prevTarget !== node) {\n forceUpdate({});\n }\n }, []);\n\n // ============ Effect: Manage Observer Lifecycle ============\n useEffect(() => {\n // SSR guard\n if (!isSupported) {\n return;\n }\n\n // Clean up existing observer\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n\n // Don't create observer if disabled\n if (!enabled) {\n return;\n }\n\n // Don't create if triggerOnce already triggered\n if (triggerOnce && hasTriggeredRef.current) {\n return;\n }\n\n // Don't create if no target\n if (!targetRef.current) {\n return;\n }\n\n const target = targetRef.current;\n\n // Create and observe function\n const createAndObserve = () => {\n observerRef.current = new IntersectionObserver(handleIntersection, {\n threshold,\n root,\n rootMargin,\n });\n\n if (target) {\n observerRef.current.observe(target);\n }\n };\n\n // Handle delay\n if (delay > 0) {\n delayTimeoutRef.current = setTimeout(() => {\n createAndObserve();\n delayTimeoutRef.current = null;\n }, delay);\n\n return () => {\n if (delayTimeoutRef.current) {\n clearTimeout(delayTimeoutRef.current);\n delayTimeoutRef.current = null;\n }\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n }\n\n // Create observer immediately\n createAndObserve();\n\n // Cleanup\n return () => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n enabled,\n triggerOnce,\n delay,\n threshold,\n root,\n rootMargin,\n handleIntersection,\n isSupported,\n // Include target change trigger\n targetRef.current,\n ]);\n\n // ============ Effect: Reset hasTriggered when triggerOnce is disabled ============\n useEffect(() => {\n if (!triggerOnce) {\n hasTriggeredRef.current = false;\n }\n }, [triggerOnce]);\n\n // ============ SSR Return ============\n if (!isSupported) {\n return {\n entry: initialIsIntersecting ? createInitialEntry(true, null) : null,\n inView: initialIsIntersecting,\n ref: createNoopRef(),\n };\n }\n\n return {\n entry,\n inView,\n ref: setRef,\n };\n}\n","import type { IntersectionEntry } from \"./types\";\n\n/**\n * Check if IntersectionObserver API is supported in the current environment\n * Returns false in SSR environments or browsers without support\n */\nexport function isIntersectionObserverSupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n \"IntersectionObserver\" in window\n );\n}\n\n/**\n * Convert a native IntersectionObserverEntry to our IntersectionEntry type\n * Provides a consistent interface with additional convenience properties\n *\n * @param nativeEntry - The native IntersectionObserverEntry from the browser\n * @returns IntersectionEntry with all properties\n */\nexport function toIntersectionEntry(\n nativeEntry: IntersectionObserverEntry\n): IntersectionEntry {\n return {\n entry: nativeEntry,\n isIntersecting: nativeEntry.isIntersecting,\n intersectionRatio: nativeEntry.intersectionRatio,\n target: nativeEntry.target,\n boundingClientRect: nativeEntry.boundingClientRect,\n intersectionRect: nativeEntry.intersectionRect,\n rootBounds: nativeEntry.rootBounds,\n time: nativeEntry.time,\n };\n}\n\n/**\n * Create an initial IntersectionEntry for SSR or before first observation\n * Used when initialIsIntersecting is true\n *\n * @param isIntersecting - Whether to set initial state as intersecting\n * @param target - Optional target element (null for SSR)\n * @returns A mock IntersectionEntry\n */\nexport function createInitialEntry(\n isIntersecting: boolean,\n target: Element | null = null\n): IntersectionEntry | null {\n if (!isIntersecting) {\n return null;\n }\n\n // Create a placeholder DOMRect for SSR\n const emptyRect: DOMRectReadOnly = {\n x: 0,\n y: 0,\n width: 0,\n height: 0,\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n toJSON: () => ({}),\n };\n\n // Create a mock native entry\n const mockNativeEntry = {\n target: target as Element,\n isIntersecting,\n intersectionRatio: isIntersecting ? 1 : 0,\n boundingClientRect: emptyRect,\n intersectionRect: emptyRect,\n rootBounds: null,\n time: typeof performance !== \"undefined\" ? performance.now() : Date.now(),\n } as IntersectionObserverEntry;\n\n return {\n entry: mockNativeEntry,\n isIntersecting,\n intersectionRatio: isIntersecting ? 1 : 0,\n target: target as Element,\n boundingClientRect: emptyRect,\n intersectionRect: emptyRect,\n rootBounds: null,\n time: mockNativeEntry.time,\n };\n}\n\n/**\n * Normalize threshold to always be an array\n * Handles both single number and array inputs\n *\n * @param threshold - Single threshold or array of thresholds\n * @returns Array of threshold values\n */\nexport function normalizeThreshold(\n threshold: number | number[] | undefined\n): number[] {\n if (threshold === undefined) {\n return [0];\n }\n if (Array.isArray(threshold)) {\n return threshold;\n }\n return [threshold];\n}\n\n/**\n * Deep compare two IntersectionObserverInit options objects\n * Used to determine if observer needs to be recreated\n *\n * @param a - First options object\n * @param b - Second options object\n * @returns true if options are equal\n */\nexport function areOptionsEqual(\n a: IntersectionObserverInit,\n b: IntersectionObserverInit\n): boolean {\n // Compare root\n if (a.root !== b.root) {\n return false;\n }\n\n // Compare rootMargin\n if (a.rootMargin !== b.rootMargin) {\n return false;\n }\n\n // Compare threshold (normalize to arrays for comparison)\n const thresholdA = normalizeThreshold(a.threshold);\n const thresholdB = normalizeThreshold(b.threshold);\n\n if (thresholdA.length !== thresholdB.length) {\n return false;\n }\n\n for (let i = 0; i < thresholdA.length; i++) {\n if (thresholdA[i] !== thresholdB[i]) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Create a no-op ref callback for SSR environments\n * Returns a function that does nothing when called\n */\nexport function createNoopRef(): (node: Element | null) => void {\n return () => {\n // No-op for SSR\n };\n}\n\n/**\n * Validate rootMargin string format\n * rootMargin follows CSS margin syntax: \"10px\", \"10px 20px\", \"10px 20px 30px 40px\"\n *\n * @param rootMargin - The rootMargin string to validate\n * @returns true if valid format\n */\nexport function isValidRootMargin(rootMargin: string): boolean {\n // Basic validation - rootMargin should contain px or %\n // Browser will handle more detailed validation\n const pattern = /^(-?\\d+(\\.\\d+)?(px|%)?\\s*){1,4}$/;\n return pattern.test(rootMargin.trim());\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@usefy/use-intersection-observer",
3
+ "version": "0.0.1",
4
+ "description": "A React hook for observing element visibility using Intersection Observer API with enterprise-grade features",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "peerDependencies": {
20
+ "react": "^18.0.0 || ^19.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@testing-library/jest-dom": "^6.9.1",
24
+ "@testing-library/react": "^16.3.1",
25
+ "@testing-library/user-event": "^14.6.1",
26
+ "@types/react": "^19.0.0",
27
+ "jsdom": "^27.3.0",
28
+ "react": "^19.0.0",
29
+ "rimraf": "^6.0.1",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.0.0",
32
+ "vitest": "^4.0.16"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/mirunamu00/usefy.git",
40
+ "directory": "packages/use-intersection-observer"
41
+ },
42
+ "license": "MIT",
43
+ "keywords": [
44
+ "react",
45
+ "hooks",
46
+ "intersection-observer",
47
+ "viewport",
48
+ "visibility",
49
+ "lazy-loading",
50
+ "infinite-scroll",
51
+ "scroll-animation",
52
+ "useIntersectionObserver"
53
+ ],
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "dev": "tsup --watch",
57
+ "test": "vitest run",
58
+ "test:watch": "vitest",
59
+ "typecheck": "tsc --noEmit",
60
+ "clean": "rimraf dist"
61
+ }
62
+ }