-
-
Save gragland/d1175eb983772b077cb17ae0841c5329 to your computer and use it in GitHub Desktop.
import { useState, useEffect, useRef } from 'react'; | |
// Usage | |
function App() { | |
// Ref for the element that we want to detect whether on screen | |
const ref = useRef(); | |
// Call the hook passing in ref and root margin | |
// In this case it would only be considered onScreen if more ... | |
// ... than 300px of element is visible. | |
const onScreen = useOnScreen(ref, '-300px'); | |
return ( | |
<div> | |
<div style={{ height: '100vh' }}> | |
<h1>Scroll down to next section 👇</h1> | |
</div> | |
<div | |
ref={ref} | |
style={{ | |
height: '100vh', | |
backgroundColor: onScreen ? '#23cebd' : '#efefef' | |
}} | |
> | |
{onScreen ? ( | |
<div> | |
<h1>Hey I'm on the screen</h1> | |
<img src="https://i.giphy.com/media/ASd0Ukj0y3qMM/giphy.gif" /> | |
</div> | |
) : ( | |
<h1>Scroll down 300px from the top of this section 👇</h1> | |
)} | |
</div> | |
</div> | |
); | |
} | |
// Hook | |
function useOnScreen(ref, rootMargin = '0px') { | |
// State and setter for storing whether element is visible | |
const [isIntersecting, setIntersecting] = useState(false); | |
useEffect(() => { | |
const observer = new IntersectionObserver( | |
([entry]) => { | |
// Update our state when observer callback fires | |
setIntersecting(entry.isIntersecting); | |
}, | |
{ | |
rootMargin | |
} | |
); | |
if (ref.current) { | |
observer.observe(ref.current); | |
} | |
return () => { | |
observer.unobserve(ref.current); | |
}; | |
}, []); // Empty array ensures that effect is only run on mount and unmount | |
return isIntersecting; | |
} |
If you pass []
as inputs you should cache ref.current
in a local variable in the effect - otherwise you are risking a leak because the ref is mutable and can change over time - therefore u might call unobserve with the wrong element
@ianobermiller Good call, updated!
I agree. I do believe ref.current
should be passed as argument of useEffect
line 58. Currently if the ref changes, nothing happens and the previous is leaked.
You may also consider returning the ref from the hook instead of passing it as argument, c.f. https://usehooks.com/useHover/.
observer.unobserve(ref.current);
In the useEffect
, we should keep a reference to the ref.current
like this: const currentRef = ref.current
.
Then do: observer.unobserve(currentRef);
In doing so, we will ensure that ref.current
doesn't end up changing before this cleanup is run.
I made a couple small changes:
https://gist.github.com/ianobermiller/1146d469e22561f88a9b4d81ea477e4c
root
entirely, since it defaults to the viewport anyway (alsodocument.querySelector('body').current
is always undefined, could bedocument.body
but isn't needed anyway)margin
torootMargin
for clarity and conciseness