Custom React Hook to Trigger Callback when Component Enters Screen
Mar 8, 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