@@ -25,6 +25,11 @@ export function useAnimatedCount(targetValue, elementRef) {
2525 const lastSeenValueRef = useRef ( /** @type {number | null } */ ( null ) ) ;
2626 const wasInViewportRef = useRef ( false ) ;
2727
28+ // Track if observer has been set up to avoid duplicate setup
29+ const observerSetupRef = useRef ( false ) ;
30+ // Store observer instance for cleanup
31+ const observerRef = useRef ( /** @type {IntersectionObserver | null } */ ( null ) ) ;
32+
2833 // Memoize the update callback to avoid recreating it on every render
2934 const updateAnimatedCount = useCallback (
3035 /** @type {import('./animateCount.js').AnimationUpdateCallback } */ (
@@ -36,43 +41,88 @@ export function useAnimatedCount(targetValue, elementRef) {
3641 [ ] ,
3742 ) ;
3843
44+ // Helper function to create and set up IntersectionObserver
45+ const setupIntersectionObserver = useCallback (
46+ ( element ) => {
47+ // Prevent duplicate setup
48+ if ( observerSetupRef . current || observerRef . current ) {
49+ return ;
50+ }
51+
52+ observerSetupRef . current = true ;
53+ observerRef . current = new IntersectionObserver (
54+ ( entries ) => {
55+ entries . forEach ( ( entry ) => {
56+ const wasInViewport = wasInViewportRef . current ;
57+ const isNowInViewport = entry . isIntersecting ;
58+
59+ // When element exits viewport, save current displayed value
60+ if ( wasInViewport && ! isNowInViewport ) {
61+ lastSeenValueRef . current = animatedValueRef . current ;
62+ }
63+
64+ wasInViewportRef . current = isNowInViewport ;
65+ setIsInViewport ( isNowInViewport ) ;
66+ } ) ;
67+ } ,
68+ {
69+ // Trigger when any part of the element is visible
70+ threshold : 0 ,
71+ // Optional: add some margin to trigger slightly before visible
72+ rootMargin : '0px' ,
73+ } ,
74+ ) ;
75+
76+ observerRef . current . observe ( element ) ;
77+ } ,
78+ [ ] ,
79+ ) ;
80+
3981 // Setup IntersectionObserver for viewport detection
82+ // Effect runs once on mount and checks elementRef.current
83+ // Since refs are assigned synchronously during commit, elementRef.current should be available
84+ // But we handle the edge case where it might not be yet using a microtask
85+ // Note: elementRef is not in dependencies because ref objects maintain stable identity
4086 useEffect ( ( ) => {
4187 // If no elementRef provided, element is always considered "in viewport"
42- if ( ! elementRef || ! elementRef . current ) {
88+ if ( ! elementRef ) {
4389 setIsInViewport ( true ) ;
4490 return ;
4591 }
4692
47- const observer = new IntersectionObserver (
48- ( entries ) => {
49- entries . forEach ( ( entry ) => {
50- const wasInViewport = wasInViewportRef . current ;
51- const isNowInViewport = entry . isIntersecting ;
52-
53- // When element exits viewport, save current displayed value
54- if ( wasInViewport && ! isNowInViewport ) {
55- lastSeenValueRef . current = animatedValueRef . current ;
56- }
57-
58- wasInViewportRef . current = isNowInViewport ;
59- setIsInViewport ( isNowInViewport ) ;
60- } ) ;
61- } ,
62- {
63- // Trigger when any part of the element is visible
64- threshold : 0 ,
65- // Optional: add some margin to trigger slightly before visible
66- rootMargin : '0px' ,
67- } ,
68- ) ;
69-
70- observer . observe ( elementRef . current ) ;
93+ // Check if element is available immediately
94+ const element = elementRef . current ;
95+ let cancelled = false ;
96+
97+ if ( element ) {
98+ // Element is available, set up observer immediately
99+ setupIntersectionObserver ( element ) ;
100+ } else {
101+ // If element not available, use a microtask to check again
102+ // This handles edge cases where ref assignment is delayed
103+ Promise . resolve ( ) . then ( ( ) => {
104+ // Check if effect was cleaned up before microtask executed
105+ if ( cancelled ) {
106+ return ;
107+ }
108+
109+ const delayedElement = elementRef . current ;
110+ if ( delayedElement ) {
111+ setupIntersectionObserver ( delayedElement ) ;
112+ }
113+ } ) ;
114+ }
71115
72116 return ( ) => {
73- observer . disconnect ( ) ;
117+ cancelled = true ;
118+ observerSetupRef . current = false ;
119+ if ( observerRef . current ) {
120+ observerRef . current . disconnect ( ) ;
121+ observerRef . current = null ;
122+ }
74123 } ;
75- } , [ elementRef ] ) ;
124+ // eslint-disable-next-line react-hooks/exhaustive-deps
125+ } , [ ] ) ; // Empty array: run once on mount. elementRef is stable, setupIntersectionObserver is memoized
76126
77127 // Animate count when it changes, page is visible, and element is in viewport
78128 useEffect ( ( ) => {
0 commit comments