← Back toMaster of React ref

Pass a ref to a custom hook

Written byPhuoc Nguyen
Created
20 Oct, 2023
In our previous post, we learned how to use the `usePrevious()` hook to determine if an element is in view. If it is, we trigger an animation. We used the example of showcasing different types of products, each represented by a card with an image and some information.
To make the site more engaging, we've added an animation that fades in and scales the product when the user scrolls to the corresponding card.
Today, we're taking this example a step further by creating an infinite loading feature using the same technique. Additionally, you'll learn how to pass a ref created by the `useRef()` hook to a custom hook and handle additional events to the element represented by the ref.

Understanding infinite loading

Infinite loading, also known as endless scrolling, is a popular technique used by many websites to dynamically load data. Instead of presenting the user with a pagination system or a "Load More" button, infinite loading allows the user to scroll down the page and automatically loads more data when they reach the end.
This technique is particularly useful when dealing with large datasets that would otherwise take a long time to load on initial page load. By loading only small chunks of data at a time, you can greatly improve your website's performance and provide a smoother user experience. Say goodbye to endless waiting and hello to seamless browsing!

Creating an infinite loading list

Let's talk about implementing infinite loading on your webpage. By detecting when an element comes into view while scrolling, we can add more elements to the page and fetch additional data once users reach the bottom.
To keep things simple, let's assume each product on the list has only one piece of information: its name. We'll use the product's index as its name to make it easy to see how it works.
To start, we'll create 20 products with indexes ranging from 1 to 20. As we dynamically load more data, we'll store the list of products in an internal `products` state and use `isFetching` to indicate whether we're currently fetching data from our database. By default, `isFetching` is set to `false`.
Here's a code snippet to initialize our states:
tsx
const initialProducts = Array(20)
.fill(0)
.map((_, index) => ({
name: `${index + 1}`,
}));

const [isFetching, setIsFetching] = React.useState(false);
const [products, setProducts] = React.useState(initialProducts);
Our component will display a list of products along with a loading indicator at the bottom. The indicator lets users know that new products are being fetched, so they're aware of what's happening. The loading indicator is only shown when `isFetching` is `true`. This enhances the user experience. Here's how the component should render:
tsx
{/* List of products */}
<div className="grid">
{
products.map((product, index) => (
<div key={index}>
{product.name}
</div>
))
}
</div>

{/* Loading indicator */}
{isFetching && (
<div className="loading">
<div className="loading__inner">
Loading more data ...
</div>
</div>
)}
So far, everything's been pretty straightforward. But now we need to figure out how to know when users have scrolled to the bottom of the page so we can load more products for them.
To solve this problem, all we need to do is add a special element at the bottom of the product listings.
tsx
{/* List of products */}
<div className="grid">
...
</div>

<div ref={bottomRef} />

{/* Loading indicator */}
...
In this example, we're adding a `div` element and assigning the `ref` attribute to a reference that can be created using `useRef`.
ts
const bottomRef = React.useRef();
In our previous post, we introduced a technique to determine when users reach the bottom of a page. We do this by checking if the `div` element we created is currently in view.
Here's a quick reminder of what we've done so far:
ts
React.useEffect(() => {
if (!wasInView && isInView) {
handleReachBottom();
}
}, [isInView]);
The `handleReachBottom()` function is triggered when users scroll to the bottom and reach the `div` element. Once this happens, we fetch more products from our database.
To keep things simple, we generate an additional 20 products and add them to the current list of products.
tsx
const fetchMoreData = () => {
const fetched = Array(20).fill(0).map((_, index) => ({
name: `${products.length + index + 1}`,
}));
setIsFetching(false);
setProducts(products.concat(fetched));
};

const handleReachBottom = () => {
setIsFetching(true);
setTimeout(fetchMoreData, 2000);
};
It's worth noting that we've used the `setTimeout` function to simulate the time it takes to fetch data. In our example, it will take 2 seconds (2000 milliseconds passed to the `setTimeout` function).
This means that when you try the demo below, you'll see the loading indicator displayed for a short while, and then it will disappear after 2 seconds when the data fetching is complete. As new products are displayed alongside the existing ones, be sure to scroll to the bottom of the page to see the loading indicator in action.

Creating a custom hook for animations and infinite loading

We've learned how to trigger an animation or an infinite loading effect using the same technique. But what if we want to add the animation to each product when it becomes visible on the screen?
To achieve this, we need to encapsulate the logic of checking if an element is in view into a custom hook. This hook should have two parameters: the `ref` representing the target element we want to check and a callback function that is triggered when the element is in view.
Here's an example of how the hook could be implemented:
ts
const useInView = (ref, onInView) => {
// ...

React.useEffect(() => {
const ele = ref.current;
if (ele && !wasInView && isInView) {
onInView(ele);
}
}, [isInView, ref]);
};
The `onInView` callback is executed when the element is in view. To see the full implementation, check out the demo at the end.
The hook is easy to reuse. For example, we can create a `Card` component to animate a product card.
tsx
const Card = ({ children }) => {
const ref = React.useRef();

const handleInView = (ele) => {
ele.classList.add("card__animated");
};

useInView(ref, handleInView);

return (
<div className="card" ref={ref}>{children}</div>
);
};
In this example, we've passed the `handleInView` callback to add the `card__animated` CSS class to the card when its content is in view. To enable infinite loading, we just need to pass the ref and function to the `useInView` hook like this:
ts
useInView(bottomRef, handleReachBottom);
When users scroll to the bottom of the page, the bottom `div` becomes visible, which triggers the `handleReachBottom` function. This function displays a loading indicator and fetches more products for us. So, we can keep scrolling without interruption and have a seamless user experience.

Demo

Check out the final demo! Scroll down to the bottom to see what we've been up to.

Conclusion

By putting the logic of checking if an element is in view into a custom hook, you can easily use it again and again in your code and trigger different actions based on what the user does. To use the hook, you just need to create a reference to the element you want to track, and the hook takes care of the rest.
In fact, you can use the same approach for a wide range of real-life situations.
If you found this post helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn