Fareez Ahamed

Custom React Hook to Trigger Callback when Component Enters Screen

March 08, 2020

Sometimes we would like to postpone loading data in a component till it is actually visible in the viewport. This is handy in applications where we have lots of tables with data that should be loaded only when we scroll down. This behavior could be used in a variety of cases in different components, so we are going to make it into a reusable custom hook so that we can use it wherever we would like to.

So now we are going to create a sample application where we have a table at the end of the page. When we scroll down we have to fetch data from the Star Wars api and fill it in the table.

Custom hooks are simple, they behave very much similar to Functional Components. You can use useState and useEffect hooks in a custom hook exactly as you would use in any Functional Component.

Our application will have the following structure. A large empty div followed by a table which will load some data and display in the screen as we scroll down to that table.

import React, { useState, useEffect } from "react"

function App() {
  return (
    <div className="max-w-screen-md mx-auto">
      {/* Push the div large enough to force the */}
      {/* scroll in the document */}
      <div
        style={{
          backgroundColor: "#cccccc",
          height: "100rem",
        }}
      ></div>
      {/* Component that loads data when enters screen */}
      <StarWarsTable> </StarWarsTable>
    </div>
  )
}

For acheiving this we have to access the scroll event and a reference to the DOM Element which we have to check if it has entered the screen. Let’s name our hook as useScreenEnter. So the following code is how it will look when we are going to use the hook from the StarWarsTable component.

useScreenEnter(ref, () => {
  // Fetch data
  fetch(
    "https://swapi.co/api/people?format=json"
  ).then(/* do what you want to do with the data here */)
})

So let’s focus on writing the custom hook now. Following code is the definition of the hook, with a list of things that has to be done within the hook.

export function useScreenEnter(ref, callback) {
  /**
     1. Start listening to the DOM Element
        to see if the element is in screen

     2. If it is in the screen call the callback

     3. Make sure the callback executes only once
        and not on repeated enter and exit of the
        element in the viewport
  */
}

How are we going to do this? We can simply use useState and useEffect within our custom hook as we do in Functional Component.

  • Keep a state variable to check if the Element has entered screen once
  • Listen to the scroll event of the document and keep checking if the DOM Element has fall within the viewport
  • Trigger the callback when the DOM Element entered the viewport and set that it has entered once, so that we don’t have to call the callback again
export function useScreenEnter(ref, callback) {
  const [entered, setEntered] = useState(false)

  function activate() {
    if (
      ref.current &&
      isInViewPort(ref.current.getBoundingClientRect()) &&
      !entered
    ) {
      callback()
      setEntered(true)
    }
  }

  useEffect(() => {
    document.addEventListener("scroll", activate)
    return () => document.removeEventListener("scroll", activate)
  })
}

If you are interested in how we calculate if the element is in viewport, the following function does that.

function isInViewPort(rect) {
  if (
    window.screen.height >= rect.bottom &&
    window.screen.width >= rect.right &&
    rect.top >= 0 &&
    rect.left >= 0
  )
    return true
  return false
}

Now let’s turn to the StarWarsTable component which we had in the App component at the beginning.

const StarWarsTable = props => {
  const [chars, setChars] = useState([])
  const [loading, setLoading] = useState(false)
  const ref = React.createRef()

  useScreenEnter(ref, () => {
    // Show loading
    setLoading(true)

    // Fetch data
    fetch("https://swapi.co/api/people?format=json")
      .then(res => res.json())
      .then(data => {
        setChars(data.results)
        setLoading(false)
      })
  })

  return (
    <div ref={ref}>
      {loading ? (
        <p>Loading...</p>
      ) : (
        <table className="">
          <thead>
            <tr>
              <td>Name</td>
              <td>Gender</td>
              <td>Height</td>
              <td>Mass</td>
            </tr>
          </thead>
          <tbody>
            {chars.map(person => (
              <tr key={person.name}>
                <td>{person.name}</td>
                <td>{person.gender}</td>
                <td>{person.height}</td>
                <td>{person.mass}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  )
}

That’s it! Hooks have really made extracting complex logics that happens during different lifecycle events into reusable logic pretty easily.

You can find the code on Github