Create a reference using React.createRef()
Written byPhuoc Nguyen
Created
09 Oct, 2023
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:
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.
#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:
#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:
#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:
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!
#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
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 🥷.
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