DEV Community

Cover image for Pass a ref to a custom hook
Phuoc Nguyen
Phuoc Nguyen

Posted on • Updated on • Originally published at phuoc.ng

Pass a ref to a custom hook

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:

const initialProducts = Array(20)
    .fill(0)
    .map((_, index) => ({
        name: `${index + 1}`,
    }));

const [isFetching, setIsFetching] = React.useState(false);
const [products, setProducts] = React.useState(initialProducts);
Enter fullscreen mode Exit fullscreen mode

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:

{/* 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>
)}
Enter fullscreen mode Exit fullscreen mode

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.

{/* List of products */}
<div className="grid">
    ...
</div>

<div ref={bottomRef} />

{/* Loading indicator */}
...
Enter fullscreen mode Exit fullscreen mode

In this example, we're adding a div element and assigning the ref attribute to a reference that can be created using useRef.

const bottomRef = React.useRef();
Enter fullscreen mode Exit fullscreen mode

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:

React.useEffect(() => {
    if (!wasInView && isInView) {
        handleReachBottom();
    }
}, [isInView]);
Enter fullscreen mode Exit fullscreen mode

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.

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);
};
Enter fullscreen mode Exit fullscreen mode

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:

const useInView = (ref, onInView) => {
    // ...

    React.useEffect(() => {
        const ele = ref.current;
        if (ele && !wasInView && isInView) {
            onInView(ele);
        }
    }, [isInView, ref]);
};
Enter fullscreen mode Exit fullscreen mode

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.

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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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:

useInView(bottomRef, handleReachBottom);
Enter fullscreen mode Exit fullscreen mode

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.


It's highly recommended that you visit the original post to play with the interactive demos.

If you found this series 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!

If you want more helpful content like this, feel free to follow me:

Top comments (0)