← Back toMaster of React ref

Reference an element with React's useRef() hook

Written byPhuoc Nguyen
Created
14 Oct, 2023
Tags
Carousel slider, useRef() hook, Web animation API
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:
tsx
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>
);
};
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.
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?
tsx
<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>
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:
tsx
<Slider>
{
Array(5).fill(0).map((_, index) => (
<div key={index}>{index + 1}</div>
))
}
</Slider>
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.
tsx
const cloneChildren = React.Children.toArray(children);
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:
tsx
<div className="slider__inner">
{
cloneChildren.map((children, index) => (
<div
className="slider__item"
key={index}
style={{
transform: `translateX(${100 * index}%)`,
}}
>
{children}
</div>
))
}
</div>
We can use the same approach to render dots that allow us to navigate to a specific item.
tsx
<div className="slider__navigation">
{
cloneChildren.map((_, index) => (
<div className="slider__dot" key={index} />
))
}
</div>
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:
tsx
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);
}
};
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:
tsx
<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>
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.
tsx
const innerRef = React.useRef();
To attach a reference to the inner element, simply use the `ref` attribute.
tsx
<div className="slider__inner" ref={innerRef}>
{
cloneChildren.map((children, index) => (
...
))
}
</div>
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:
tsx
const jump = (index) => {
const innerEle = innerRef.current;
if (!innerEle) {
return;
}
innerEle.animate([
{
transform: `translateX(${-100 * index}%)`,
},
], {
duration: 400,
easing: 'ease-in-out',
fill: 'forwards',
});
};
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.
tsx
innerEle.animate(..).addEventListener('finish', () => {
setCurrentIndex(index);
});

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.
tsx
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>
To animate the active dot, we need to retrieve the navigation and active dot elements using the refs that were created earlier.
tsx
const jump = (index) => {
const navigationEle = navigationRef.current;
const activeDotEle = activeDotRef.current;
// ...
};
Next, we calculate the left offset of the target dot by using its index and the `offsetLeft` property.
tsx
const dots = [...navigationEle.querySelectorAll('.slider__dot')];
const left = dots[index].offsetLeft;
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:
tsx
activeDotEle.animate([
{
transform: `translateX(${left}px)`,
},
], {
duration: 400,
easing: 'ease-in-out',
fill: 'forwards',
});
By calling `activeDotEle.animate()` with these arguments, we animate the active dot element to smoothly transition from one position to another.

Demo

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

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