← Back toMaster of React ref

Store a reference with callback refs

Written byPhuoc Nguyen
Created
10 Oct, 2023
We've learned how to use string refs to create references to elements within a class component. However, string refs are now considered legacy code and can cause performance issues. Callback refs are more efficient and flexible, making them the preferred option for referencing DOM elements within a component.
The best part is that callback refs can be used with both class and functional components. This flexibility is a game-changer for developers who prefer functional components over class components.
Using callback refs also allows you to directly access the underlying DOM element, which is much easier than using `this.refs` as you would with string refs. This makes it simpler to interact with specific elements within your component without relying on complicated state management or external libraries.
In this post, we'll dive into what callback refs are and how to use them to build a real-world example. But first, let's take a look at the syntax of callback refs.

Understanding the syntax

Callback refs in React are a way to pass a function as a ref instead of an object. This allows us to use the corresponding DOM element in other parts of our code.
For example, let's say we have a component that renders an input field. We can create a callback ref function within our component and pass it to the `ref` attribute of the input element. When the component mounts, React calls this function with the DOM element as its argument, making it easily accessible for us to use.
tsx
<input
ref={(ele) => (this.inputEle = ele)}
onChange={this.handleChange}
/>
In this example, the callback function is executed when the component is mounted, and it sets the inner `inputEle` variable to the input element. This means we can directly manipulate the input element using its reference.
For instance, we can handle the `onChange` event to update the input value.
tsx
handleChange() {
this.setState({
value: this.inputEle.value,
});
}
To get the current value of the input element, you can use `inputEle.value` where `inputEle` is the reference to the input element. Once you have the new value, you can set it back to the input. Here's a code sample to help you out:
tsx
render() {
const { value } = this.state;
return (
<input value={value} />
);
}

Building an input counter

Let's dive into building a real-world component that we call `InputCounter`. This component will display the number of remaining characters that users can type in an input field.
Why is this useful? Showing the number of remaining characters in a text input can be a helpful feature for users. It provides immediate feedback on how much they can continue to type, which can prevent them from going over the maximum character limit. This is especially useful in situations where there are limits on the amount of text that can be entered, such as when filling out a form or composing a tweet.
But that's not all. Displaying the number of remaining characters can also help with accessibility. Users who rely on screen readers or other assistive technologies may have difficulty determining the length of their input without this visual indicator.
Overall, implementing an input counter is a simple and effective way to improve the usability and accessibility of your forms and inputs. So, let's get started!
Let's start by organizing the component's markup. It's a simple `div` with two inner elements: one for the input field and the other for displaying the remaining characters.
tsx
<div className="container">
<input className="container__input" />
<div className="container__counter"></div>
</div>
To spruce up the appearance of our component, we can add some styles to the CSS classes attached to our elements.
For example, we can add a border around the entire container and remove the default border from the input. Additionally, we can use CSS flex to organize the container, aligning the child elements horizontally within it.
To ensure the input maintains its shape and size while taking up all available space within the container, we set its `flex` property to 1.
Here are the basic styles for the classes:
css
.container {
border: 1px solid rgb(203 213 225);
border-radius: 0.25rem;
display: flex;
align-items: center;
}
.container__input {
border: none;
flex: 1;
}

Counting characters

Calculating the number of remaining characters is easy. We just need to keep track of two things: the current value and how many characters the user can still input. To make the component more adaptable, we assume that it has a configuration prop called `maxCharacters` that limits the number of characters allowed.
Here's how we set up the states:
tsx
constructor(props) {
super(props);
this.state = {
remainingChars: this.props.maxCharacters,
value: '',
};
}
Whenever the user changes the input value, we update the relevant states accordingly. To handle the input's `onChange` event, we use the `handleChange()` function. Here it is:
tsx
handleChange() {
const newValue = this.inputEle.value;
const remainingChars = this.props.maxCharacters - newValue.length;
this.setState({
value: newValue,
remainingChars,
});
}
The calculation used in the `handleChange()` function is pretty straightforward. We subtract the length of the new value from the maximum number of characters allowed, and set the result as the remaining number of characters. Easy peasy, right?
These states are then used to render the remaining character count, so users can see how many characters they have left to type.
tsx
render() {
const { remainingChars, value } = this.state;
return (
<input
className="container__input"
value={value}
ref={(ele) => this.inputEle = ele}
onChange={this.handleChange}
/>
<div className="container__counter">
{remainingChars}
</div>
);
}

Enhancing user experience with smooth animations

Let's take the user experience up a notch by adding a smooth animation that warns users when they are about to reach the last character. With the state of the remaining characters calculated earlier, we can dynamically add a CSS class to the counter element.
tsx
<div className={`container__counter ${remainingChars < 1 ? 'container__counter--warning' : ''}`}>
{remainingChars}
</div>;
When there are no characters left to input, the counter element adds the warning class. You can customize the warning styles to your liking, such as adding a bold red color to make it stand out.
css
.container__counter--warning {
color: rgb(239 68 68);
font-weight: 600;
}
But wait, there's more! We can take it a step further by adding a simple CSS animation. This not only improves the usability and accessibility of your components, but also provides visual feedback that enhances their functionality and makes them more engaging for users.
css
.container__counter--warning {
animation: scale 200ms;
}

@keyframes scale {
0% {
transform: scale(1);
}
100% {
transform: scale(1.5);
}
}
In this example, adding the warning class to an element causes it to gradually scale up from 1 to 1.5 over 200 milliseconds. This creates a subtle yet noticeable effect that draws the user's attention to the warning message without being too intrusive.
To achieve this animation, we use the `@keyframes` rule to define how the element should change over time. In this case, we start with a scale of 1 and gradually increase it to 1.5 over the course of 200ms.
To see it in action, try typing more characters in the input field until the warning styles appear.
However, there's a problem: once the counter reaches 0, if users keep typing in the input field, the animation won't trigger anymore. This is because the warning class has already been added to the counter element.
To fix this, we need to add and remove the warning class dynamically. This is where callback refs come in handy. Instead of adding the warning class inside the `render` function, we'll manage it at the appropriate time.
Let's start by adding a callback ref to the counter element using the `ref` attribute.
tsx
<div className={`container__counter`} ref={(ele) => (this.counterEle = ele)}>
{remainingChars}
</div>
By doing this, we can retrieve the counter element using the `counterEle` variable. Then, we'll update the `handleChange()` function to add the warning class to the counter element dynamically when necessary.
tsx
handleChange() {
// ...
if (remainingChars < 1) {
this.counterEle.classList.add('container__counter--warning');
}
}
So, when should we remove the warning class from the counter element? The answer is simple: we should remove it once the animation has finished its job. We can easily handle this by using the `onAnimationEnd` event, which triggers automatically when the animation is done.
tsx
handleAnimationEnd() {
this.counterEle.classList.remove('container__counter--warning');
}

render() {
return (
<div
ref={(ele) => this.counterEle = ele}
onAnimationEnd={this.handleAnimationEnd}
>
...
</div>
);
}
In this example, the warning class is removed from the counter element after the animation runs for 200 milliseconds. If users continue typing, the warning class is added back to the counter element, which triggers the animation again.
Now, let's take a look at the final demo:
As mentioned earlier, callback refs have an advantage over string refs as they can be used in functional components as well. Here's another version that demonstrates the use of callback refs in a functional component:
Stay tuned for upcoming posts where we'll delve into more real-life examples that showcase the advantages of callback refs!
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