In our last post, we learned about using the findDOMNode()
function to access a component's underlying DOM node. However, this only gives us access to the root node. What if we want to access other nodes in the component's markup tree?
Imagine we're building an accordion component with multiple items, each with their own markup. We can identify the body of each item with the item__body
CSS class, and the content with the item__content
class.
<div className="item">
<button className="item__heading">
<span className="item__arrow" />
{/* Title */}
</button>
<div className="item__body">
<div className="item__content">{/* Content */}</div>
</div>
</div>
We could try accessing these elements by starting at the root node and using querySelector()
const node = ReactDOM.findDOMNode(this);
const bodyEle = node.querySelector('.item__body');
const contentEle = node.querySelector('.item__content');
But that doesn't feel very "React-y", and it's not very reliable. If another engineer changes the CSS classes, the component could break.
In this post, we'll show you how to use string refs to avoid those issues and make your component more maintainable.
Syntax of string refs
To create a reference to an element, we simply pass a string as a ref
attribute on that element. Here's an example:
<div className="item__body" ref="body">
...
</div>
In this example, we create a reference called body
using a string and attach it to an element with the ref
attribute. We can then access the underlying DOM node using this.refs.body
.
Building an accordion item component
As with the rest of this series, we learn best by doing. In this post, we'll build an accordion item component that lets users expand or collapse content by clicking the heading.
To keep track of whether an item is open or closed, we'll use an internal boolean state called isOpened
. By default, the content is hidden, so we set the state to false
initially.
Here's an example of what the AccordionItem
component could look like:
class AccordionItem extends React.Component {
constructor(props) {
super(props);
this.state = {
isOpened: false,
};
}
}
Clicking the heading will toggle the isOpened
state. In the code below, we handle the click
event of the heading element and use the setState
function to toggle the state:
handleToggle() {
this.setState((prevState) => ({
isOpened: !prevState.isOpened,
}));
}
render() {
return (
<button
className="item__heading"
onClick={this.handleToggle}
>
{this.props.title}
</button>
);
}
The content is only shown when the item is expanded:
render() {
const { isOpened } = this.state;
return (
<div className="item__body">
{isOpened && (
<div className="item__content">
{this.props.children}
</div>
)}
</div>
);
}
So far, nothing too fancy. We've used basic things like the setState
function to manage internal states of a class component and render things accordingly depending on the state.
Now, let's stop reading and play around with the accordion item component in the demo below by clicking its heading:
Adding animation to improve user experience
Let's take the accordion item component to the next level by adding an animation when users expand or collapse the content. But before we dive into how we can achieve this in React, let's explore how we can add animation in CSS.
The most common way to do this is by using the transition
property. By changing the height of the body element, we can run an animation over a duration of, say, 250 milliseconds.
.item__body {
overflow: hidden;
transition: height 250ms;
}
But how do we determine the width of the item body? That's where string refs come in. Since we have full control over setting refs to any element we want, we can set the refs for the body and content elements as follows:
<div className="item__body" ref="body">
<div className="item__content" ref="content">
{/* Content */}
</div>
</div>
Initially, the item is collapsed, so we set the height
property of the body element to zero:
<div
className="item__body"
ref="body"
style={{
height: 0,
}}
>
...
</div>
Next, we need to modify the handleToggle()
function. Instead of changing the isOpened
state instantly, we need to update the height of the body element to trigger the animation. Here's how the modified version of handleToggle()
function could look like:
handleToggle() {
const { isOpened } = this.state;
this.refs.body.style.height = !isOpened
? this.refs.content.clientHeight
: 0;
}
In this example, if the item is being opened, we set the height of its body element to be equal to its content element's client height to display it smoothly with an animation effect. On the other hand, if the item is being closed, we set its body element's height back to zero.
Thanks to string refs, we can access the body and content elements by the name of associated refs which are this.refs.body
and this.refs.content
.
By updating only the height
property of the CSS style instead of changing all styles for each toggle action, we can achieve a smooth animation effect without any performance issues.
Now, we need to update the isOpened
state accordingly when the animation is completed. We can do this by handling the transitionEnd
event of the body element:
handleTransitionEnd() {
this.setState((prevState) => ({
isOpened: !prevState.isOpened,
}));
}
render() {
return (
<div
className="item__body"
onTransitionEnd={this.handleTransitionEnd}
>
...
</div>
);
}
Enhancing your component with arrows
For a more polished and professional look, consider adding an arrow to the heading of your component to indicate its expanding state. Don't worry if you're not sure how to do this – we've got you covered. Check out this helpful post for a details guide on how to add arrows to your design.
Ready to see the final product? Take a look at our demo and see for yourself how much of a difference this simple addition can make.
The limits of String refs
String refs can be helpful in certain situations, but they have some limitations that can cause issues and make them less flexible than other techniques, such as callback refs or ref hooks. Here are the main limitations of using string refs in React components:
- They can't be used with functional components: functional components don't have instances, so they can't support string refs because they work by creating a reference to the underlying instance.
- They may cause naming collisions: if you use string refs multiple times within a component, there's a risk of naming collisions between different refs. This can happen if two or more elements have the same ref name.
- They don't provide access to the current value of the ref: unlike callback refs, which allow you to access the current value of the ref at any time, string refs only give you access to the DOM node when it's being mounted or unmounted.
- They are not as flexible as callback refs: since string refs rely on attaching a name directly to an element, they're not as flexible as callback refs, which allow you to pass arguments and additional data along with the ref function.
Overall, while string refs can be useful in certain scenarios, their limitations make them less versatile and more prone to errors than other techniques. Since React 16.3, string refs have been deprecated in favor of other techniques for better performance and flexibility. Although string refs are no longer recommended, they were an important concept and React has been improved with more powerful refs based on this older concept.
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)