Instantly share code, notes, and snippets.

Embed
What would you like to do?
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;
}
@mattfysh

This comment has been minimized.

mattfysh commented Nov 9, 2018

This is great! thanks :) 👍

One question on if (ref.current) - given the empty array on line 59, the effect only runs once on mount. Is it possible that ref.current is falsy, and the effect misses its only chance to observer.observe(ref.current)?

If yes - should the effect be run again at some point in the future?
If no - do we need the if (ref.current) gatecheck?

@ianobermiller

This comment has been minimized.

ianobermiller commented Nov 9, 2018

I made a couple small changes:

https://gist.github.com/ianobermiller/1146d469e22561f88a9b4d81ea477e4c

  1. You can remove root entirely, since it defaults to the viewport anyway (also document.querySelector('body').current is always undefined, could be document.body but isn't needed anyway)
  2. Rename margin to rootMargin for clarity and conciseness
@Andarist

This comment has been minimized.

Andarist commented Nov 10, 2018

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

@gragland

This comment has been minimized.

Owner

gragland commented Nov 20, 2018

@ianobermiller Good call, updated!

@gragland

This comment has been minimized.

Owner

gragland commented Nov 20, 2018

@mattfysh and @Andarist: Good points! So I'm thinking it would make sense to pass ref.current in input array so that effect is recalled if it changes and then wrap entire effect function body in if (ref.current) { ... } so that it does nothing if ref.current is falsy. Thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment