DEV Community

Cover image for Create a reference using React.createRef()
Phuoc Nguyen
Phuoc Nguyen

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

Create a reference using React.createRef()

In our previous post, we covered how to use a string ref to create a reference to an element in the markup. But as we learned, string refs have some limitations. For one, there's a risk of naming collisions between different refs if you use them multiple times within a component. Additionally, string refs only give you access to the DOM node, so they can't be used for storing references to other values.

But don't worry, because in this post we'll introduce you to a new solution: React.createRef(). This method offers a way to address these issues and take your React skills to the next level!

Using React.createRef() in a class component

To use React.createRef() in a class component, you'll need to create a ref object first by calling the createRef() method in the constructor of the component. Once you have your ref object, you can attach it to a specific element in the render method.

Here's an example to help you get started:

class Collapse extends React.Component {
    constructor(props) {
        super(props);
        this.bodyRef = React.createRef();
    }

    render() {
        return (
            <div ref={this.bodyRef}>...</div>
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we made a ref object called bodyRef using the createRef() method in the component's constructor. Then, in the render method, we linked this ref object to a div element using the ref attribute.

Accessing the DOM element

After assigning the ref attribute to an element, we can easily access the underlying DOM element using the current property of the ref object.

Here's an example:

class Collapse extends React.Component {
    componentDidMount() {
        // Outputs the DOM element
        console.log(this.bodyRef.current);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we accessed the DOM element using the current property of the ref object in the componentDidMount() lifecycle method.

Building a collapse component

Now that you have a basic understanding of the syntax of the ref created by the createRef method, let's use that knowledge to create a collapse component.

A collapse component is a great tool for toggling the visibility of content on a web page. For example, imagine you have a long article with multiple sections, and you want to allow your readers to collapse or expand each section as they read through the article. This can help keep the page organized and make it easier for readers to find the information they're looking for.

Another great use case for a collapse component is in a navigation menu. You can collapse submenus that are not currently being used, making it easier for users to navigate through the main menu items.

In short, a collapse component is useful any time you have content that needs to be hidden or shown based on user interaction. It provides an elegant and intuitive way for users to interact with your website or application. So let's get started building one!

Our Collapse component is responsible for collapsing and expanding content. This is how we organize its markup:

<div className="item__body truncate">
    <div>{this.props.children}</div>
</div>
<button
    className="item__toggle"
    onClick={this.handleToggle}
>
    More
</button>
Enter fullscreen mode Exit fullscreen mode

At the top, there's the body element with a class called item__body. Inside that element, there's a div that contains all the content. At the bottom of the element, there's a button that you can use to expand or collapse the content.

To collapse the content, we limit the first three lines using the line-clamp property in CSS. The truncate class is added to limit the number of lines displayed, effectively truncating the content. This is useful when you have a lot of text that you want to display in a small space.

.truncate {
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
}
Enter fullscreen mode Exit fullscreen mode

To determine whether the component is expanded or collapsed, we use an internal state named isOpened. By default, the content is collapsed, so the value of isOpened is set to false.

class Collapse extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isOpened: false,
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

The handleToggle() function is used to expand or collapse the component depending on the current state. In simpler terms, it just reverses the value of the isOpened state.

handleToggle() {
    this.setState((prevState) => ({
        isOpened: !prevState.isOpened,
    }));
}
Enter fullscreen mode Exit fullscreen mode

To update the content of the Collapse component based on the isOpened state, we use a ternary operator in the className attribute of the body element. If isOpened is true, we remove the truncate class so that all of the content is displayed. If it's false, we add the class so that only the first three lines are displayed.

Similarly, we change the text of the button depending on whether the component is expanded or collapsed. If it's expanded, we change it to "Less" to indicate that clicking it will collapse the content. If it's collapsed, we change it to "More" to indicate that clicking it will expand the content.

With this implementation, clicking on the "More" button expands and collapses the content as expected, and updates its text accordingly. Check out the code below to see it in action:

render() {
    const { isOpened } = this.state;

    return (
        <div>
            <div className={`item__body ${isOpened ? '' : 'truncate'}`}>
                <div>{this.props.children}</div>
            </div>
            <button
                className="item__toggle"
                onClick={this.handleToggle}
            >
                {isOpened ? 'Less' : 'More'}
            </button>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Ready to give it a try? Check out our playground to see the basic version of the Collapse component in action.

Adding animation

In this post, we'll be taking the Collapse component to the next level by adding some animation. Our goal is to create a seamless and smooth experience for users when they expand or collapse the content. To achieve this, we'll be using the transition property in our CSS to keep track of the height property.

.item__body {
    overflow: hidden;
    transition: height 250ms;
}
Enter fullscreen mode Exit fullscreen mode

To activate the animation, we'll need to calculate the height of the body element before and after expanding or collapsing. Rather than using string refs, we'll use the createRef() function to declare references to the body and content elements.

class Collapse extends React.Component {
    constructor(props) {
        this.bodyRef = React.createRef();
        this.contentRef = React.createRef();
    }

    render() {
        return (
            <div className="item__body" ref={this.bodyRef}>
                <div ref={this.contentRef}>{this.props.children}</div>
            </div>
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

To start, we'll use the componentDidMount() life-cycle method to set the initial height of the body element to its current height. First, we'll get a reference to the body element using our bodyRef ref object. Then, we'll access its clientHeight property, which represents its current height. Finally, we'll use JavaScript string interpolation to set this value as an inline style on the element.

componentDidMount() {
    const bodyEle = this.bodyRef.current;
    bodyEle.style.height = `${bodyEle.clientHeight}px`;
}
Enter fullscreen mode Exit fullscreen mode

To calculate the final height of the transition, we'll use the handleToggle() function to get references to the body and content elements using our ref objects. Then, we check if the component is being opened or closed based on the isOpened state.

If it's being opened, we remove the truncate class from the body element so that all of its content is displayed, and set its height to equal its scrollHeight property. The scrollHeight property returns the total height of an element, including padding and borders, but not margins.

If it's being closed, we temporarily add the truncate class back to the content element to calculate its new height after truncation. We then remove this class and get its clientHeight property instead. Finally, we set this new height as an inline style on the body element using JavaScript string interpolation.

Here's the updated version of the handleToggle() function:

handleToggle() {
    const { isOpened } = this.state;
    const bodyEle = this.bodyRef.current;
    const contentEle = this.contentRef.current;

    if (!isOpened) {
        bodyEle.classList.remove('truncate');
        bodyEle.style.height = `${contentEle.scrollHeight}px`;
    } else {
        contentEle.classList.add('truncate');
        const newHeight = contentEle.clientHeight;
        contentEle.classList.remove('truncate');
        bodyEle.style.height = `${newHeight}px`;
    }
}
Enter fullscreen mode Exit fullscreen mode

When we click the button to expand or collapse the content, it triggers a change in state for isOpened. However, because we're animating the height of the body element, it takes some time for the animation to complete and for the actual height of the element to change. To make sure that our state accurately reflects whether the component is expanded or collapsed after the animation is complete, we'll use the transitionEnd event to update the state. This way, we can ensure that our component works smoothly and seamlessly for the user.

handleTransitionEnd() {
    this.setState((prevState) => ({
        isOpened: !prevState.isOpened,
    }));
}

render() {
    return (
        <div
            ref={this.bodyRef}
            onTransitionEnd={this.handleTransitionEnd}
        >
            ...
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

We've made some changes to our Collapse component. Now, when expanding or collapsing, the height animates smoothly and the state updates correctly after the animation finishes.

Check it out:

Storing the initial height

In the previous section, we added and removed the truncate class from the body element temporarily to calculate the collapsed height. However, there is a better way to do this. We can use a ref to store the initial height, which is already calculated in componentDidMount().

Using createRef() method has an advantage over string ref because it can be used for other references besides the DOM node.

To begin, let's create a reference to track the initial height.

class Collapse extends React.Component {
    constructor(props) {
        this.initialHeightRef = React.createRef(0);
    }
}
Enter fullscreen mode Exit fullscreen mode

Do you remember how we calculate and set the initial height for the body element using the componentDidMount() life-cycle method? Well, this time we're going to store the height in our new reference by setting the current property.

componentDidMount() {
    const bodyEle = this.bodyRef.current;
    this.initialHeightRef.current = bodyEle.clientHeight;
}
Enter fullscreen mode Exit fullscreen mode

When users collapse the content, we can retrieve the initial height and set it to the height of the body element. To get the value stored in a ref, we can use the current property. Here's an example of what the handleToggle() function might look like:

handleToggle() {
    const { isOpened } = this.state;

    if (!isOpened) {
        ...
    } else {
        const newHeight = this.initialHeightRef.current;
        bodyEle.style.height = `${newHeight}px`;
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's take a closer look and see how it works:

Enhancing the user experience with a fading effect

Creating a fading effect at the bottom of a component can greatly improve the user experience. By gradually fading out the content towards the bottom, users are visually prompted that there is more content available to view.

To achieve this effect, we can add a pseudo-element ::before to our CSS for item__body. The ::before element is positioned at the bottom of its parent and has a height of 2rem. We then use a linear gradient to create a fadeout effect from transparent to white.

Here's how we can update our CSS:

.item__body {
    position: relative;
}
.item__body::before {
    content: '';
    position: absolute;
    bottom: 0;

    height: 2rem;
    width: 100%;

    background: linear-gradient(rgba(203 213 225 / 0.05), #fff);
}
Enter fullscreen mode Exit fullscreen mode

With this change, our Collapse component now has a smooth animation when expanding or collapsing, and provides a visual cue for long content using the fadeout effect at the bottom.

Let's check out the final demo to see it in action!

Limitations of using createRef()

When using React.createRef(), keep in mind that it only works with class components. If you're using functional components, you'll need to use the useRef() hook instead. But don't worry, we'll cover that in an upcoming post.

Another downside of React.createRef() is that it only stores a reference to the most recent instance of an element or component. This means that if you have multiple instances of the same component on a page, you won't be able to tell them apart using refs alone.

Moreover, because refs are mutable, they can make your code harder to reason about and debug. It's possible for one part of your code to change the value of a ref without other parts being aware of it.

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)