Create a reference using React.createRef()
Tags: react, tutorial, webdev, javascript
Published: 2023-12-04 13:20:08

In our previous post, we covered how to use a [string ref](https://phuoc.ng/collection/react-ref/string-refs/) 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:

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

    render() {
        return (
            <div ref={this.bodyRef}>...</div>
        );
    }
}
```

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:

```tsx
class Collapse extends React.Component {
    componentDidMount() {
        // Outputs the DOM element
        console.log(this.bodyRef.current);
    }
}
```

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:

```tsx
<div className="item__body truncate">
    <div>{this.props.children}</div>
</div>
<button
    className="item__toggle"
    onClick={this.handleToggle}
>
    More
</button>
```

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.

```css
.truncate {
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
}
```

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`.

```tsx
class Collapse extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isOpened: false,
        };
    }
}
```

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.

```tsx
handleToggle() {
    this.setState((prevState) => ({
        isOpened: !prevState.isOpened,
    }));
}
```

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:

```tsx
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>
    );
}
```

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

{% codesandbox 6k3mj6 %}

## 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.

```css
.item__body {
    overflow: hidden;
    transition: height 250ms;
}
```

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.

```tsx
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>
        );
    }
}
```

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.

```tsx
componentDidMount() {
    const bodyEle = this.bodyRef.current;
    bodyEle.style.height = `${bodyEle.clientHeight}px`;
}
```

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:

```tsx
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`;
    }
}
```

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.

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

render() {
    return (
        <div
            ref={this.bodyRef}
            onTransitionEnd={this.handleTransitionEnd}
        >
            ...
        </div>
    );
}
```

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:

{% codesandbox klyw5g %}

## 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.

```tsx
class Collapse extends React.Component {
    constructor(props) {
        this.initialHeightRef = React.createRef(0);
    }
}
```

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.

```tsx
componentDidMount() {
    const bodyEle = this.bodyRef.current;
    this.initialHeightRef.current = bodyEle.clientHeight;
}
```

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:

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

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

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

{% codesandbox p2r42c %}

## 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](https://phuoc.ng/collection/css-layout/fading-long-section/) 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:

```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);
}
```

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!

{% codesandbox q9sxg5 %}

## 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

-   [Fading long section](https://phuoc.ng/collection/css-layout/fading-long-section/)

---

It's highly recommended that you visit the [original post](https://phuoc.ng/collection/react-ref/create-a-reference-using-react-create-ref/) to play with the interactive demos.

If you found this series helpful, please consider giving the [repository](https://github.com/phuocng/master-of-react-ref) 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:

-   [Twitter](https://twitter.com/_phuocng)
-   [GitHub](https://github.com/phuocng)