What is hidden behind React's refs ?
March 11th, 2020 - 10 min read
There was a time I used to struggle when manipulating the refs
in my react components.
Sometimes they returned undefined
, sometimes they didn't return the DOM element I was looking for. To be honest, I didn't know anything about what I was doing. I was a total noob. I used to try any combination possible until my ref
looked like what I wanted to get in the beginning.
And even then, I had no clue on how to use the ref
properly. One day, I decided to dig into the official documentation and React's code to unravel this mystery once and for all.
You probably already are familiar with refs
handling in React. But if you are not, I have good hopes this article will help you a lot.
Refs and React
First things first. What is a ref
in React ?
It is very important to understand that a ref
can be two distinct entities:
- a React component
- a DOM element (like a
<div>
)
In both cases, the ref
is a node and has a type
- it is an instance
of a JavaScript Class with methods & properties. We will go through the details in the next section.
refs
allow you to manipulate an element outside of React's lifecycle flow, and to access some of its properties & methods in an imperative way.
Note: While it is discouraged to overuse the
refs
, it can sometimes provide an efficient solution to a complex UI problem.
When to use refs ?
Why should we use refs
anyway ? Isn't the React Top-Level API enough ? Well, in some cases it is not.
Like the React's docs state it, refs
can be used for managing the focus on an element, do operations with text selection, trigger imperative animations and sometimes integrate
third-parties to your components.
Still a bit unclear? No problem. Let's go through a perfect use-case (according to me) of the refs
.
Imagine you are building a credit-card payment form. You will need an <input>
field for the credit card's number. Of course, you want to provide a good UX to your users, so
you want to shape the value of the card with the following pattern: XXXX XXXX XXXX XXXX
.
When the user enters the 4 first digits of the credit card, you want the cursor to jump to the 5th position and add a space after the last digit: 4242 |
.
Bad news: there is no way to do this without hacks or using the React API. Good News: refs
will make you able to achieve that.
You can indeed move the cursor of the <input>
to the position of your choice using the native JavaScript API.
The two methods that will move that cursor for you are setSelectionRange() & focus().
Well, the ref
you attached to the <input>
contains those two methods!
Here is a live demo of this component. Now let's go through the technical details.
The hidden truth
I swear to tell the truth, and only the truth.
Now we know that refs
are not just random objects, let's dive into the depth of their implementation.
When a ref is attached to a DOM element
Before we go through examples, let's define clearly what is the instanced class of a ref
that was attached to a DOM element.
All DOM elements follow this architecture:
A DOM element:
- implements the interface of its tag. For example, like in the above screenshot, a
<div>
implements the interface HTMLDivElement - implements the interface HTMLElement
- is an instance of the class Element
This simply means that the ref
you attach to a DOM element like a <div>
will be an object that inherits all the properties & methods the class and the interfaces offer. That said, you will be able, for example, to access the different properties of your DOM element's width from the ref
(like clientWidth
, offsetWidth
and so on).
Also, React will add some hidden properties & methods to the ref
object. But these are treated like private stuff - you don't need to use those or even know their existence.
Depending on the browser (for example some properties & methods are not available in our worst nightmare - yes, I'm talking about IE 😵), your ref
will look a bit like that:
// the content of a ref attached to a <div>
{
"__reactEventHandlers$q0kumw04tr": Object { children: (4) […], … },
"__reactInternalInstance$q0kumw04tr": Object { tag: 5, elementType: "div", type: "div", … },
accessKey: "",
accessKeyLabel: "",
align: "",
assignedSlot: null,
attributes: NamedNodeMap [],
childElementCount: 4,
childNodes: NodeList(4) [ div, div, h1, … ],
children: HTMLCollection(4) [ div, div, h1, … ],
classList: DOMTokenList [],
className: "",
clientHeight: 160,
clientLeft: 0,
clientTop: 0,
clientWidth: 77,
...
...
and many more stuff!
}
When a ref is attached to a React component
There might be some edge cases (very rare) where you might want to attach a ref
to a React component.
refs
cannot be attached to functional components as is - it requires to use the React.forwardRef
HOC we'll see in details later.
Functional components, or stateless components, cannot have a ref
attached because there is no state to attach the ref
to. It's as simple as that. Like said previously, the hooks solve this problem since react@^16.8.0.
However, you can attach a ref
to a Class Component.
In this case, your ref
:
- is an instance of your Class Component
- extends the class React.Component
That said, you will have access to the state
, the props
and the context
of the component your ref
was attached to.
Your ref
will look like that:
// the content of the ref you attached to your React component
{
_reactInternalFiber: Object { tag: 1, key: null, index: 0, … },
_reactInternalInstance: Object { … },
context: Object { },
props: Object { … },
refs: Object { },
state: null,
updater: Object { isMounted: isMounted(component), enqueueSetState: enqueueSetState(...),
...
...
And some more stuff!
}
Just like for the DOM element, React will add some private properties & methods in the ref
- that's for the internal usage of React. You will never need to access those.
Alright, enough with the theory. Now you know exactly what you are handling, let's see how to use React's refs API! 🤘
Creating the ref
There are actually two methods in order to create a ref
. In both cases, the method returns an object that will always have the following structure:
// content of your ref
{
current: HTMLElement | React.Component | undefined
}
So you will be able to access your targeted node (React or DOM) via
ref.current
Creating a ref
couldn't be more straight-forward - if you are in a class component, you can use the React.createRef method.
A good practice is to create the ref
in your constructor
:
class YourComponent extends React.Component {
constructor(props) {
super(props)
this.ref = React.createRef()
}
}
Inside a functional component, you can use the React.useRef hook. Here's how to use it:
const YourComponent = () => {
const ref = React.useRef()
}
Now your ref
is initialized and ready to be attached to a React class component or a DOM element. Let's see how to use it.
Using the ref
Once initialized, your ref
needs to be attached to something. If you want to attach it to a React class component:
class DummyComponent extends React.Component {
render() {
return <p>I am a dummy component</p>
}
}
class YourComponent extends React.Component {
constructor(props) {
super(props)
this.ref = React.createRef() // mutable object
}
componentDidMount() {
// reactNode: DummyComponent
const reactNode = this.ref.current
}
render() {
return <DummyComponent ref={this.ref} />
}
}
If you want to attach it to a DOM element:
// You could use a class component as well.
const YourComponent = () => {
const ref = React.useRef() // mutable object
React.useEffect(() => {
// divNode: HTMLDivElement
const divNode = ref.current
}, [])
return <div ref={ref}>I am a div</div>
}
Forwarding the ref with forwardRef
Finally getting there. If you read the previous part (which I trust you to have done 🙂) you may wonder what is React.forwardRef there for ? After all, it seems at first glance that we got all we need already.
Well, not quite so. We are missing one problem: what if you want the ref
you attached to a React component to go to the first DOM element it renders ? You cannot. As seen previously, the ref
you attach to a React component will return the instance of this very component.
Fortunately for us, React thought about this and offers us a simple solution: React.forwardRef
.
Note: It is an HOC. So, in brief, it's a function that takes a React component as parameter, and also returns a React component.
It looks like:
// returns a React component
React.forwardRef((props, ref) => <YourComponent forwardedRef={ref} {...props} />)
Its sole purpose is to inform your React component to pass the ref
you attached to it down to its props
instead of itself. So you can pass it to the DOM element of your chosing in its render.
Here's how to use it:
const DummyComponent = React.forwardRef((props, ref) => {
const { children } = props
return <div ref={ref}>{children}</div>
})
const YourComponent = () => {
const ref = React.useRef()
React.useEffect(() => {
// divNode: HTMLDivElement
const divNode = ref.current
}, [])
return <DummyComponent ref={ref}>I am a dummy div</DummyComponent>
}
In the above example,
divNode
is of typeHTMLDivElement
and it contains the "I am a dummy div" text.
Bonus: sharing forwarded and local refs
There are cases where you might need, at the same time, to have a local ref and also to forward this local ref to the component's parent.
This can happen for example when you are implementating an input-like component, and need to access the HTMLInputElement
's ref
locally but also to forward it to the parent.
Here's the secret: React.useImperativeHandle! 🤐
This method will enable you to expose the local ref
to the parent, while keeping the same
source of truth.
You can use it this way:
React.useImperativeHandle(forwardedRef, () => localRef.current)
Here's a more tangible example:
const MyInput = React.forwardRef((props, ref) => {
const localRef = React.useRef()
React.useImperativeHandle(ref, () => localRef.current)
return <input ref={localRef} />
})
const YourComponent = () => {
const ref = React.useRef()
React.useEffect(() => {
const inputNode = ref.current
}, [])
return <MyInput ref={ref} />
}
Now you are able to handle the input's ref
in both the parent and child components!
Note:
inputNode
andlocalRef
share the same reference.
Conclusion
I really hope I made the whole ref
party much more clear to you now, and that you can't wait to try out the bonus example! 😉
refs
system can be quite difficult to understand at first, but once
you know what is the type of the elements your are handling and how the refs
work behind the scene, you realize that it's quite fun in the end.
While React offers us these tools, they strongly discourage us from over-using them because their imperative pattern goes against the asynchronous pattern of React's components lifecycle.
However there are some UI problems that cannot be solved simply: refs
can come handy in those situations.
If you have a question, a remark, or want to bark how crap you found this article, don't hesitate to dm me on Twitter! You will find the link below.
Thanks for reading! 😃
Useful links
What you can read for further reading:
- React docs
- Components-extra's CreditCardNumber: it implements the bonus technique.
- Ankit Singh's post on Hackernoon