@donotdev/components 0.0.4 → 0.0.6

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.
@@ -1,39 +1,92 @@
1
1
  // packages/components/src/hooks/useIntersectionObserver.ts
2
2
  /**
3
- * @fileoverview Intersection Observer Hook
4
- * @description React hook for observing element intersection with viewport. Provides intersection state and entry data for scroll-based animations.
5
- *
3
+ * @fileoverview useIntersectionObserver Hook - Basic Intersection Observation
4
+ * @description Lightweight hook for basic intersection observation with optimal performance
5
+ * @package @donotdev/components
6
6
  * @version 0.0.1
7
7
  * @since 0.0.1
8
8
  * @author AMBROISE PARK Consulting
9
+ *
10
+ * Features:
11
+ * - CSR/SSR safe with proper fallbacks
12
+ * - Intersection Observer API with automatic cleanup
13
+ * - Once-only triggering option
14
+ * - TypeScript support with full type safety
15
+ * - Zero dependencies beyond React and framework utils
16
+ *
17
+ * Performance:
18
+ * - Uses native IntersectionObserver API
19
+ * - React 19 useSyncExternalStore pattern for optimal performance
20
+ * - Prevents tearing in concurrent rendering
21
+ * - Automatic cleanup prevents memory leaks
22
+ * - Minimal state updates with proper change detection
9
23
  */
10
- import { useRef, useEffect, useState, useCallback } from 'react';
11
- import { observeElement } from '../utils/intersectionObserver';
24
+ import { useRef, useEffect, useSyncExternalStore, useMemo } from 'react';
12
25
  /**
13
- * Hook for intersection observer functionality
26
+ * useIntersectionObserver - Basic Intersection Observation Hook (React 19 Optimized)
27
+ *
28
+ * Lightweight hook for basic intersection observation with optimal performance.
29
+ * Uses React 19's useSyncExternalStore pattern for automatic cleanup, proper
30
+ * SSR handling, and tearing prevention in concurrent rendering.
31
+ *
32
+ * @param options - Configuration options for intersection observation
33
+ * @returns Object containing ref, isIntersecting state, and entry
34
+ *
35
+ * @example Basic intersection detection
36
+ * ```tsx
37
+ * function IntersectionComponent() {
38
+ * const { ref, isIntersecting } = useIntersectionObserver();
39
+ *
40
+ * return (
41
+ * <div ref={ref} className={isIntersecting ? 'visible' : 'hidden'}>
42
+ * Content that appears when intersecting
43
+ * </div>
44
+ * );
45
+ * }
46
+ * ```
14
47
  *
15
- * Provides a simple way to detect when an element enters/exits the viewport.
16
- * Replaces Framer Motion's useInView with our own implementation.
48
+ * @example With custom threshold
49
+ * ```tsx
50
+ * function ThresholdComponent() {
51
+ * const { ref, isIntersecting } = useIntersectionObserver({
52
+ * threshold: 0.5,
53
+ * rootMargin: '50px'
54
+ * });
55
+ *
56
+ * return (
57
+ * <div ref={ref}>
58
+ * {isIntersecting ? '50% visible' : 'Not visible enough'}
59
+ * </div>
60
+ * );
61
+ * }
62
+ * ```
17
63
  *
18
- * @example Basic usage
64
+ * @example Once-only triggering (React 19 optimized)
19
65
  * ```tsx
20
- * const { ref, isIntersecting } = useIntersectionObserver({
21
- * threshold: 0.3
22
- * });
66
+ * function OnceComponent() {
67
+ * const { ref, isIntersecting } = useIntersectionObserver({ once: true });
23
68
  *
24
- * return (
25
- * <div>
26
- * {isIntersecting ? 'Visible!' : 'Not visible'}
27
- * </div>
28
- * );
69
+ * return (
70
+ * <div ref={ref}>
71
+ * {isIntersecting && <ExpensiveComponent />}
72
+ * </div>
73
+ * );
74
+ * }
29
75
  * ```
30
76
  *
31
- * @example With root margin (equivalent to Framer Motion's margin)
77
+ * @example SSR-safe implementation
32
78
  * ```tsx
33
- * const { ref, isIntersecting } = useIntersectionObserver({
34
- * threshold: 0.3,
35
- * rootMargin: '-70% 0px -30% 0px' // Trigger when 30% visible
36
- * });
79
+ * function SSRComponent() {
80
+ * const { ref, isIntersecting } = useIntersectionObserver({
81
+ * fallbackIntersecting: true // Shows content during SSR
82
+ * });
83
+ *
84
+ * return (
85
+ * <div ref={ref}>
86
+ * {isIntersecting ? 'Client-side' : 'SSR fallback'}
87
+ * </div>
88
+ * );
89
+ * }
37
90
  * ```
38
91
  *
39
92
  * @version 0.0.1
@@ -41,38 +94,128 @@ import { observeElement } from '../utils/intersectionObserver';
41
94
  * @author AMBROISE PARK Consulting
42
95
  */
43
96
  export function useIntersectionObserver(options = {}) {
44
- const { threshold = 0.1, rootMargin = '0px', once = false } = options;
97
+ const { threshold = 0, root = null, rootMargin = '0px', once = false, fallbackIntersecting = true, } = options;
45
98
  const ref = useRef(null);
46
- const [isIntersecting, setIsIntersecting] = useState(false);
47
- const [hasTriggered, setHasTriggered] = useState(false);
48
- const [entry, setEntry] = useState(null);
49
- // Intersection observer callback
50
- const handleIntersection = useCallback((intersectionEntry) => {
51
- const isIntersectingNow = intersectionEntry.isIntersecting;
52
- if (isIntersectingNow && (!hasTriggered || !once)) {
53
- setIsIntersecting(true);
54
- setHasTriggered(true);
55
- }
56
- else if (!isIntersectingNow && !once) {
57
- setIsIntersecting(false);
58
- }
59
- setEntry(intersectionEntry);
60
- }, [once, hasTriggered]);
61
- // Set up intersection observer
62
- useEffect(() => {
63
- const element = ref.current;
64
- if (!element)
65
- return;
66
- const cleanup = observeElement(element, handleIntersection, {
99
+ // Create intersection observer using React 19 patterns
100
+ const observer = useMemo(() => {
101
+ return createIntersectionObserver({
67
102
  threshold,
103
+ root,
68
104
  rootMargin,
105
+ once,
106
+ fallbackIntersecting,
69
107
  });
70
- return cleanup;
71
- }, [handleIntersection, threshold, rootMargin]);
108
+ }, [threshold, root, rootMargin, once, fallbackIntersecting]);
109
+ // Use useSyncExternalStore for optimal performance and proper tearing prevention
110
+ const state = useSyncExternalStore(observer.subscribe, observer.getSnapshot, observer.getServerSnapshot);
111
+ // Update observer when ref element changes
112
+ // Empty deps intentional: observer is stable from useMemo, ref updates are tracked internally
113
+ useEffect(() => {
114
+ if (ref.current) {
115
+ observer.setElement(ref.current);
116
+ }
117
+ return () => observer.setElement(null);
118
+ }, [observer]);
72
119
  return {
73
120
  ref,
74
- isIntersecting,
75
- hasTriggered,
76
- entry,
121
+ isIntersecting: state.isIntersecting,
122
+ hasTriggered: state.hasTriggered,
123
+ entry: state.entry,
124
+ };
125
+ }
126
+ /**
127
+ * Create an intersection observer with React 19 patterns
128
+ * Follows same pattern as createLocalStorageObserver for consistency
129
+ */
130
+ function createIntersectionObserver(options) {
131
+ const { threshold, root, rootMargin, once, fallbackIntersecting } = options;
132
+ let currentState = {
133
+ isIntersecting: fallbackIntersecting,
134
+ hasTriggered: false,
135
+ entry: null,
136
+ };
137
+ let listeners = new Set();
138
+ let intersectionObserver = null;
139
+ let currentElement = null;
140
+ let hasTriggered = false;
141
+ const handleIntersection = (entries) => {
142
+ const [entry] = entries;
143
+ if (!entry)
144
+ return;
145
+ const isIntersecting = entry.isIntersecting;
146
+ // Handle once option - disconnect after first intersection
147
+ if (once && isIntersecting && !hasTriggered) {
148
+ hasTriggered = true;
149
+ if (intersectionObserver) {
150
+ intersectionObserver.disconnect();
151
+ intersectionObserver = null;
152
+ }
153
+ }
154
+ // Update hasTriggered if intersecting for first time
155
+ if (isIntersecting && !hasTriggered) {
156
+ hasTriggered = true;
157
+ }
158
+ const newState = {
159
+ isIntersecting,
160
+ hasTriggered,
161
+ entry,
162
+ };
163
+ // Only update if state actually changed
164
+ if (newState.isIntersecting !== currentState.isIntersecting ||
165
+ newState.hasTriggered !== currentState.hasTriggered ||
166
+ newState.entry !== currentState.entry) {
167
+ currentState = newState;
168
+ listeners.forEach((listener) => listener());
169
+ }
170
+ };
171
+ const getSnapshot = () => {
172
+ return currentState;
173
+ };
174
+ const subscribe = (callback) => {
175
+ listeners.add(callback);
176
+ // If element is already set and we're on client, start observing
177
+ if (currentElement && !intersectionObserver && typeof window !== 'undefined') {
178
+ intersectionObserver = new IntersectionObserver(handleIntersection, {
179
+ threshold,
180
+ root,
181
+ rootMargin,
182
+ });
183
+ intersectionObserver.observe(currentElement);
184
+ }
185
+ return () => {
186
+ listeners.delete(callback);
187
+ // Cleanup observer when last listener is removed
188
+ if (listeners.size === 0 && intersectionObserver) {
189
+ intersectionObserver.disconnect();
190
+ intersectionObserver = null;
191
+ }
192
+ };
193
+ };
194
+ const setElement = (element) => {
195
+ // Disconnect existing observer
196
+ if (intersectionObserver) {
197
+ intersectionObserver.disconnect();
198
+ intersectionObserver = null;
199
+ }
200
+ currentElement = element;
201
+ // Observe new element if we have listeners and we're on client
202
+ if (element && listeners.size > 0 && typeof window !== 'undefined') {
203
+ intersectionObserver = new IntersectionObserver(handleIntersection, {
204
+ threshold,
205
+ root,
206
+ rootMargin,
207
+ });
208
+ intersectionObserver.observe(element);
209
+ }
210
+ };
211
+ return {
212
+ subscribe,
213
+ getSnapshot,
214
+ getServerSnapshot: () => ({
215
+ isIntersecting: fallbackIntersecting,
216
+ hasTriggered: false,
217
+ entry: null,
218
+ }),
219
+ setElement,
77
220
  };
78
221
  }