DEV Community

Cover image for Store a reference with callback refs
Phuoc Nguyen
Phuoc Nguyen

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

Store a reference with callback refs

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.

<input
    ref={(ele) => (this.inputEle = ele)}
    onChange={this.handleChange}
/>
Enter fullscreen mode Exit fullscreen mode

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.

handleChange() {
    this.setState({
        value: this.inputEle.value,
    });
}
Enter fullscreen mode Exit fullscreen mode

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:

render() {
    const { value } = this.state;
    return (
        <input value={value} />
    );
}
Enter fullscreen mode Exit fullscreen mode

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.

<div className="container">
    <input className="container__input" />
    <div className="container__counter"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

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:

.container {
    border: 1px solid rgb(203 213 225);
    border-radius: 0.25rem;
    display: flex;
    align-items: center;
}
.container__input {
    border: none;
    flex: 1;
}
Enter fullscreen mode Exit fullscreen mode

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:

constructor(props) {
    super(props);
    this.state = {
        remainingChars: this.props.maxCharacters,
        value: '',
    };
}
Enter fullscreen mode Exit fullscreen mode

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:

handleChange() {
    const newValue = this.inputEle.value;
    const remainingChars = this.props.maxCharacters - newValue.length;
    this.setState({
        value: newValue,
        remainingChars,
    });
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

<div className={`container__counter ${remainingChars < 1 ? 'container__counter--warning' : ''}`}>
    {remainingChars}
</div>;
Enter fullscreen mode Exit fullscreen mode

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.

.container__counter--warning {
    color: rgb(239 68 68);
    font-weight: 600;
}
Enter fullscreen mode Exit fullscreen mode

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.

.container__counter--warning {
    animation: scale 200ms;
}

@keyframes scale {
    0% {
        transform: scale(1);
    }
    100% {
        transform: scale(1.5);
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

<div className={`container__counter`} ref={(ele) => (this.counterEle = ele)}>
    {remainingChars}
</div>
Enter fullscreen mode Exit fullscreen mode

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.

handleChange() {
    // ...
    if (remainingChars < 1) {
        this.counterEle.classList.add('container__counter--warning');
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

handleAnimationEnd() {
    this.counterEle.classList.remove('container__counter--warning');
}

render() {
    return (
        <div
            ref={(ele) => this.counterEle = ele}
            onAnimationEnd={this.handleAnimationEnd}
        >
            ...
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

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!


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)