DEV Community

Avraam Mavridis
Avraam Mavridis

Posted on

My first React hook

This is not an introduction to hooks, or any kind of tip or best practices, at the end hooks were just announced, so we have to play around with them and wait to see how the community embrace them, and the best practices that will come up. This is just my experience of implementing a hook.

I wanted to create a hook that will use the Intersection Observer API, to track the visibility of a functional component. Whenever the visibility change (component on viewport or not) I want the hook to update the state of the component.

I am gonna call my hook useVisibility.

Intersection Observer needs access to the DOM node it's gonna observer:

var target = document.querySelector('#listItem');
observer.observe(target);

So useRef seems the right hook to create a reference to our component's markup

function HelloComponent(){
  const nodeRef = useRef(null);

  return (
    <div className="App" ref={nodeRef}>
      <h1>Hello</h1>
    </div>
  );
}

Apart from the node reference, Intersection Observer accepts some options, this can be the second parameter of our hook, so the signature of it can be:

  const visibility = useVisibility(nodeRef, options); 

Let's write the actual hook now, we will need the useState hook of React to track the visibility state

function useVisibility(node, options = {}) {
  const [ visible, setVisibilty ] = useState({});
  ...

Then we are gonna create an instance of the Intersection Observer passing a callback and the options.

const observer = new IntersectionObserver(handleObserverUpdate, options);

To start calling the callback we need to tell the observer to start observing our node, since this is a browser API "outside of react world", we are gonna use the useEffect hook provided by React.

  useEffect(() => {
    if (node.current) {
      observer.observe(node.current);
    }
  });

Our code has a memory leak though, we need to provide a way to cleanup, for this reason we are returning a callback that when is called tells our observer to stop watching the node.

  useEffect(() => {
    if (node.current) {
      observer.observe(node.current);
    }

    return function cleanup() {
      observer.unobserve(node.current);
    };
  });

Finally our handler will look like this:

  const isIntersecting = useRef();

  const handleObserverUpdate = (entries) => {
    const ent = entries[0];

    if (isIntersecting.current !== ent.isIntersecting) {
      setVisibilty(ent);
      isIntersecting.current = ent.isIntersecting;
    }
  };

Now, you may wonder why I am using const isIntersecting = useRef() since this is not a DOM node.

As the docs state:

Note that useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.

I wanted a ref attribute that persist for the full lifetime of the component.

Summarising, personally I found the new API very simple and super useful, and I am very excited with what the solutions and best practices the community come up in the next months.

To be honest though, testing was not as easy as expected, mainly because of the need to mock a browser API like this, and not because of the hook API. Most testing frameworks come with utils to simulate clicks, input changes etc, but some APIs need manual mocking and depending on their complexity that can be tricky.

You can find the hook here

GitHub logo AvraamMavridis / react-intersection-visible-hook

React Hook to track the visibility of a functional component

react-intersection-visible-hook

React hook to track the visibility of a functional component based on IntersectionVisible Observer.

In the following example we changed the background color of the body depending on the visibility of the blue box.

Demo and tests are coming

import useVisibility from 'react-intersection-visible-hook'

How to use it

function App() {
  const nodeRef = useRef(null);
  const visibility = useVisibility(nodeRef);
  return (
    <div className="App" ref={nodeRef}>
      <h1>Hello</h1>
    </div>
  );
}
With options
const options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}
function App() {
  const nodeRef = useRef(null);
  const visibility = useVisibility(nodeRef, options);
  return (
    <div className="App"

Top comments (0)