DEV Community

Cover image for Reference an element with React's useRef() hook
Phuoc Nguyen
Phuoc Nguyen

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

Reference an element with React's useRef() hook

In the previous post, we learned about using React.createRef() to create a reference to an element and store the initial height of the Collapse component. However, this function only works with class components, leaving functional components out of the equation.

Enter useRef(), a hook introduced by React to solve this problem. In this post, we'll explore how to use this hook to create a carousel slider component. But before we dive into the syntax, let's understand how it works.

Understanding the syntax of the useRef() hook

The useRef() hook is an essential tool in React development. Luckily, its syntax is straightforward and easy to understand. Here's a quick guide on how to use it:

import * as React from "react";

export const Slider = () => {
    const innerRef = React.useRef();

    // Do something with innerRef ...

    return (
        <div className="slider">
            <div className="slider__inner" ref={innerRef}></div>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

In the example above, we create a reference to a JSX element using the React.useRef() function and store it in a variable called innerRef. By passing this reference as a prop with the name ref, we can attach it to any JSX element.

Once we have a reference to an element, we can access its properties and methods. For instance, if we want to get the height of our <div> element, we can use innerRef.current.offsetHeight.

Now that we know how to use the useRef() hook, let's put it to use and build a Slider component.

Introducing the carousel slider component

A carousel slider is an interactive component that allows users to browse through a collection of items, such as images or cards, by sliding them horizontally or vertically. It's commonly used in websites and mobile applications to showcase products, highlight features, or display news articles.

You can find carousel sliders on various types of websites and apps, including e-commerce platforms, news portals, and social media sites. They offer an engaging and space-saving way for users to navigate through content.

Carousel sliders can also be customized with different transition effects, autoplay options, and navigation controls to enhance the user experience.

To demonstrate the power of the useRef() hook, we're going to create the Slider component. Before we dive into the next section, it's recommended that you check out this post, which outlines how we use CSS to create a carousel slider.

Here's a preview of what the slider looks like without any interaction:

Making the slider more flexible

Let's take a closer look at the Slider's render() function in the demo above. Right now, we've hard-coded the items and dots inside the navigation. But what if we could accept dynamic children instead?

<div className="slider">
    <div className="slider__inner">
        <div className="slider__item" style={{ transform: "translateX(0%)" }}>
            1
        </div>
        <div className="slider__item" style={{ transform: "translateX(100%)" }}>
            2
        </div>
        <div className="slider__item" style={{ transform: "translateX(200%)" }}>
            3
        </div>
        <div className="slider__item" style={{ transform: "translateX(300%)" }}>
            4
        </div>
        <div className="slider__item" style={{ transform: "translateX(400%)" }}>
            5
        </div>
    </div>

    <div className="slider__navigation">
        <div className="slider__dot"></div>
        <div className="slider__dot"></div>
        <div className="slider__dot"></div>
        <div className="slider__dot"></div>
        <div className="slider__dot"></div>
    </div>

    <div className="slider__prev"></div>
    <div className="slider__next"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

To make the Slider component more flexible, we can allow it to accept children props. This means that by passing in children as a prop, we can dynamically render any number of items in our slider. Here's an example of how we could pass children to the Slider component:

<Slider>
{
    Array(5).fill(0).map((_, index) => (
        <div key={index}>{index + 1}</div>
    ))
}
</Slider>
Enter fullscreen mode Exit fullscreen mode

To handle these child components properly, we need to convert them into an array using React.Children.toArray(). This function ensures that we can map over our children without encountering any problems with null or undefined values.

const cloneChildren = React.Children.toArray(children);
Enter fullscreen mode Exit fullscreen mode

After cloning the children into an array, we use the map function to render each child. Here's an example code to render the items:

<div className="slider__inner">
{
    cloneChildren.map((children, index) => (
        <div
            className="slider__item"
            key={index}
            style={{
                transform: `translateX(${100 * index}%)`,
            }}
        >
            {children}
        </div>
    ))
}
</div>
Enter fullscreen mode Exit fullscreen mode

We can use the same approach to render dots that allow us to navigate to a specific item.

<div className="slider__navigation">
{
    cloneChildren.map((_, index) => (
        <div className="slider__dot" key={index} />
    ))
}
</div>
Enter fullscreen mode Exit fullscreen mode

By allowing the Slider component to accept children props and using the React.Children.toArray() method, we can create a dynamic and reusable carousel slider that can display any number of items.

Adding navigation functionality

Now that we have the layout created, it's time to add navigation functionalities. There are several ways to navigate between items: by clicking the corresponding dot or the previous or next arrow. We'll use an internal state called currentIndex to track the index of the active item.

Here's a sample code to enable this functionality:

const [currentIndex, setCurrentIndex] = useState(0);

const jump = (index) => setCurrentIndex(index);

const goToPreviousItem = () => {
    if (currentIndex > 0) {
        setCurrentIndex(currentIndex - 1);
    }
};

const goToNextItem = () => {
    const numItems = cloneChildren.length;
    if (currentIndex < numItems - 1) {
        setCurrentIndex(currentIndex + 1);
    }
};
Enter fullscreen mode Exit fullscreen mode

The jump function updates the currentIndex state with the new index of the active item. It's responsible for jumping to the selected item. The goToPreviousItem and goToNextItem functions update the active item to the previous or next item in the list. goToPreviousItem checks if there is a previous item before updating the index. If there is no previous item, it does nothing. goToNextItem checks if there is a next item before updating the index. If there is no next item, it also does nothing.

Here's a sample code for these functions:

<div className="slider__navigation">
{
    cloneChildren.map((_, index) => (
        <div
            key={index}
            onClick={() => jump(index)}
        />
    ))
}
</div>

<div className="slider__prev" onClick={goToPreviousItem}></div>
<div className="slider__next" onClick={goToNextItem}></div>
Enter fullscreen mode Exit fullscreen mode

By updating the internal state with each navigation action, we ensure that our component stays in sync with user interactions and renders accordingly.

Try clicking a dot or an arrow button to see the current item replaced by the target one. Check out the playground below to see it in action:

Enhancing slider navigation with animation

The slider now has basic navigation functionalities. However, we can take the user experience to the next level by adding animation when users navigate to a particular item.

Animation is an essential feature for any slider component. It adds a layer of sophistication and enhances the user experience by creating smooth transitions between items. Without animation, the slider can feel abrupt and jarring as it jumps from one item to another. By adding animation, we create a more seamless and enjoyable experience for our users.

To get started, we'll use the useRef() hook to create a reference to the inner element.

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

To attach a reference to the inner element, simply use the ref attribute.

<div className="slider__inner" ref={innerRef}>
{
    cloneChildren.map((children, index) => (
        ...
    ))
}
</div>
Enter fullscreen mode Exit fullscreen mode

Let's make a few changes to the jump() function. Rather than updating the internal state immediately, we'll animate the inner element by moving it to the left. Here's the updated version:

const jump = (index) => {
    const innerEle = innerRef.current;
    if (!innerEle) {
        return;
    }
    innerEle.animate([
        {
            transform: `translateX(${-100 * index}%)`,
        },
    ], {
        duration: 400,
        easing: 'ease-in-out',
        fill: 'forwards',
    });
};
Enter fullscreen mode Exit fullscreen mode

To animate the inner element in our example, we can use the current property to reference the corresponding element we created earlier.

Before animating, we should check if the innerEle variable is defined. Then, we can use the animate() method of the inner element to create the animation.

This method takes an array of keyframes and an options object. For our purposes, we only need one keyframe that specifies how far to move the element horizontally using translateX(). The options object specifies how long the animation should take (duration), what easing function to use (easing), and whether to keep the final state of the animation after it finishes (fill).

By calling innerEle.animate() with these arguments, we can create a smooth transition between items when users navigate through our carousel slider.

Once the animation completes, we update our internal state. We can do this by handling the finish event, which is triggered when the element completes its animation.

innerEle.animate(..).addEventListener('finish', () => {
    setCurrentIndex(index);
});
Enter fullscreen mode Exit fullscreen mode

Adding animation to the active dot

To enhance the user experience, we can add animation to the active dot. To do this, we will use the same approach as in the previous section. We will once again use the useRef() hook to create references to the navigation and active dot elements.

const navigationRef = React.useRef();
const activeDotRef = React.useRef();

<div className="slider__navigation" ref={navigationRef}>
    {cloneChildren.map((_, index) => (
        {/* Render dots */}
    ))}
    <div className="slider__dot--active" ref={activeDotRef} />
</div>
Enter fullscreen mode Exit fullscreen mode

To animate the active dot, we need to retrieve the navigation and active dot elements using the refs that were created earlier.

const jump = (index) => {
    const navigationEle = navigationRef.current;
    const activeDotEle = activeDotRef.current;
    // ...
};
Enter fullscreen mode Exit fullscreen mode

Next, we calculate the left offset of the target dot by using its index and the offsetLeft property.

const dots = [...navigationEle.querySelectorAll('.slider__dot')];
const left = dots[index].offsetLeft;
Enter fullscreen mode Exit fullscreen mode

To create a smooth transition between the current and target dots, we use the animate() method on the activeDotEle. This method takes an array of keyframes that specify how far to move the element horizontally using translateX(). Additionally, an options object specifies the duration of the animation, the easing function to use, and whether to keep the final state of the animation after it finishes.

Here's how we can make it happen:

activeDotEle.animate([
    {
        transform: `translateX(${left}px)`,
    },
], {
    duration: 400,
    easing: 'ease-in-out',
    fill: 'forwards',
});
Enter fullscreen mode Exit fullscreen mode

By calling activeDotEle.animate() with these arguments, we animate the active dot element to smoothly transition from one position to another.

Now let's check out the final result. Just click on the dots or arrows to see how both the target item and corresponding dot are animated at the same time.

See also


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)