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