@@ -45,7 +45,7 @@ export function useInView({
4545 fallbackInView,
4646 onChange,
4747} : IntersectionOptions = { } ) : InViewHookResponse {
48- const [ ref , setRef ] = React . useState < Element | null > ( null ) ;
48+ const ref = React . useRef < Element | null > ( null ) ;
4949 const unobserve = React . useRef < Function > ( ) ;
5050 const callback = React . useRef < IntersectionOptions [ 'onChange' ] > ( ) ;
5151 const [ state , setState ] = React . useState < State > ( {
@@ -54,21 +54,28 @@ export function useInView({
5454 } ) ;
5555
5656 // Store the onChange callback in a `ref`, so we can access the latest instance
57- // inside the `useEffect `, but without triggering a rerender.
57+ // inside the `useCallback `, but without triggering a rerender.
5858 callback . current = onChange ;
5959
60- React . useEffect (
61- ( ) => {
60+ const setRef = React . useCallback (
61+ ( node : Element ) => {
62+ ref . current = node ;
6263 // Ensure we have node ref, and that we shouldn't skip observing
63- if ( skip || ! ref ) return ;
64+ if ( skip || ! node ) return ;
6465
66+ // Store a reference the current unobserve function, so we can destroy it later
67+ const previousObserver = unobserve . current ;
68+
69+ // Create a new IntersectionObserver, and store the `unobserve` function.
6570 unobserve . current = observe (
66- ref ,
71+ node ,
6772 ( inView , entry ) => {
6873 setState ( {
6974 inView,
7075 entry,
7176 } ) ;
77+
78+ // Trigger any onChange callback function
7279 if ( callback . current ) callback . current ( inView , entry ) ;
7380
7481 if ( entry . isIntersecting && triggerOnce && unobserve . current ) {
@@ -89,20 +96,18 @@ export function useInView({
8996 fallbackInView ,
9097 ) ;
9198
92- return ( ) => {
93- if ( unobserve . current ) {
94- unobserve . current ( ) ;
95- unobserve . current = undefined ;
96- }
97- } ;
99+ if ( previousObserver ) {
100+ // Was already observing a node - Make sure we destroy the previous observer.
101+ // Do it after we create the new one, so the IntersectionObserver instance can be reused.
102+ previousObserver ( ) ;
103+ }
98104 } ,
99105 // We break the rule here, because we aren't including the actual `threshold` variable
100106 // eslint-disable-next-line react-hooks/exhaustive-deps
101107 [
102108 // If the threshold is an array, convert it to a string, so it won't change between renders.
103109 // eslint-disable-next-line react-hooks/exhaustive-deps
104110 Array . isArray ( threshold ) ? threshold . toString ( ) : threshold ,
105- ref ,
106111 root ,
107112 rootMargin ,
108113 triggerOnce ,
@@ -113,18 +118,27 @@ export function useInView({
113118 ] ,
114119 ) ;
115120
116- const entryTarget = state . entry ?. target ;
117-
121+ // We break the rule here, since we want to ensure we check the `ref` instances on every render.
122+ // eslint-disable-next-line react-hooks/exhaustive-deps
118123 React . useEffect ( ( ) => {
119- if ( ! ref && entryTarget && ! triggerOnce && ! skip ) {
124+ if ( ! ref . current && state . entry ?. target && ! triggerOnce && ! skip ) {
120125 // If we don't have a node ref, then reset the state (unless the hook is set to only `triggerOnce` or `skip`)
121- // This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView
126+ // This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView.
122127 setState ( {
123128 inView : ! ! initialInView ,
124129 entry : undefined ,
125130 } ) ;
126131 }
127- } , [ ref , entryTarget , triggerOnce , skip , initialInView ] ) ;
132+
133+ return ( ) => {
134+ if ( ! ref . current && unobserve . current ) {
135+ // We no longer have a valid ref. Destroy the observer
136+ unobserve . current ( ) ;
137+ unobserve . current = undefined ;
138+ ref . current = null ;
139+ }
140+ } ;
141+ } ) ;
128142
129143 const result = [ setRef , state . inView , state . entry ] as InViewHookResponse ;
130144
0 commit comments