useRef
allows you to keep a mutable value within a component, similar to useState
or instance variables on a class, without triggering re-renders.
For example, this component stores the number of clicks for a button:
function RefButton () {
const clicks = useRef(0)
return (
<button onClick={() => (clicks.current += 1)}>
Clicks: {clicks.current}
</button>
)
}
This is how this component looks like (I added a re-render button so you can actually test it out π):
The example below is completely interactive, try clicking the "Clicks" button and then click on "Re-render".
As you can see, if you click the "Clicks" button it doesn't do anything. However, after click on "Re-render", it gets updated with the number of clicks we did previously.
You might wonder why not just use a simple variable as the example below:
let clicks = 0;
function OutsideVariableButton() {
return (
<button onClick={() => (clicks += 1)}>
Clicks: {clicks}
</button>
)
}
And here's an interactive example for it:
The button works the same way that our previous example. However, the problem arises when you have multiple instances of the same component like the example below. Try clicking just one of the buttons and then click on re-render to see the result.
As you were able to see, the clicks are not isolated. In fact, all the examples from this article uses the same button component, so if you click the button from the first example and then click on "re-render" on the second example, the count it is gonna be incremented! What a bug π.
On the other hand, useRef
values are completely isolated between components:
The main difference between useState and useRef, is that useState triggers a re-render and useRef doesn't.
In the following example I added two buttons: one that updates its count with useRef
and the other one with useState
. I added some labels so you can identify them easily.
You'll notice that clicking on the button with useRef
doesn't trigger a re-render and thus, the view isn't updated. On the other side, when you click on the button that uses useState
, it will update its clicks count immediately.
To perform imperative actions on DOM nodes, React provides a way to get a
reference to them via refs. All you have to do is to assign a ref
property to
a node with a ref object like this:
function CustomInput() {
const inputRef = useRef()
return <input ref={inputRef} />
}
The way to get a DOM reference using refs works (informally π ) as follows:
On the first render, inputRef
's value will be { current: null }
and in the
following renders it will have its current
property assigned to the specified DOM
node:
// First render: { current: undefined }
// Second render: { current: <input /> }
// Third render: { current: <input /> }
/// ...and so on
console.log(inputRef)
However, if you only reference inputRef
inside useEffect
then it'll always
reference the DOM node so you don't need to worry about it being undefined.
Let's update our example to get an idea of how this works:
function AttachingToDomExample() {
const inputRef = useRef()
console.log("Render inputRef value:", inputRef)
useEffect(() => console.log("useEffect inputRef value:", inputRef))
return <input ref={inputRef} />
}
Here's the console output when rendering this component:
Render | Location | Value |
---|---|---|
1 | Render | { current: undefined } |
useEffect | { current: <input /> } | |
2 | Render | { current: <input /> } |
useEffect | { current: <input /> } | |
3 | Render | { current: <input /> } |
useEffect | { current: <input /> } |
As you can see, if you access the inputRef
inside useEffect
then you don't
need to worry about it being undefined
because React will assign it
automatically for you.
Let's start with a simple real-world application for refs: usePrevious
. This
hook stores the previous value for a given state variable.
It is even referenced on React's docs as a way to "get the previous props or state". Let's see it in
action first:
function UsePreviousExample() {
const [clicks, setClicks] = useState(0)
// usePrevious is the important part here β¨
const previousClicks = usePrevious(clicks)
return (
<div>
<button onClick={() => setClicks(clicks + 1)}>
Clicks: {clicks} - Before: {previousClicks}
</button>
</div>
)
}
Here's the output so you can play with it:
You can notice that the previousClicks
variable stores the value for the previous render
for a given variable. Here's its implementation:
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
Let's analyze how it works.
Let's simulate what happens on the first render. We can remove the call to
useEffect
since it doesn't affect the return value on the first render:
// First render
function usePrevious(value) {
const ref = useRef()
return ref.current
}
On the first render it is called with a value of 0
:
// First render
const previousClicks = usePrevious(0)
In this case, usePrevious
will return undefined
:
// First render
function usePrevious(value) {
const ref = useRef()
return ref.current // No assignment, so it's undefined
}
After increase the value for count, here's how the usePrevious
call will look:
// Second render
const previousClicks = usePrevious(1)
Since usePrevious
is called again, its effect needs to run:
useEffect(() => {
ref.current = 0
})
After this, the usePrevious
function is called again:
// First render
function usePrevious(value) {
const ref = useRef()
return ref.current // 0
}
And so on. Here's the value for each render for both variables:
Render | clicks | previousClicks |
---|---|---|
1 | 0 | undefined |
2 | 1 | 0 |
3 | 2 | 1 |
4 | 3 | 2 |
Callback Refs are a different way to set refs. It gives you a fine-grain control over when refs are attached and detached because you provide a function instead of a ref variable. This function gets called every time the component mounts and unmounts.
Here's an example that shows/hides an emoji every time you click its button.
The important thing here is the ref
prop that we added. We use a function to log
the provided ref:
const callback = (ref) => console.log("callback:", ref)
function App () {
const [show, setShow] = useState(true);
return (
<div>
<button onClick={() => setShow(!show)}>
{show ? "Hide" : "Show"}
</button>
{show && <span ref={callback}>π</span>}
</div>
);
}
Here's an interactive version of the previous code (you can check the output in the console to see that I'm not lying π):
Note: If you use callback refs as inline functions, it will be called
twice: one with null
and another one with the DOM element.
This is because React needs to clear the previous ref every time the function is
created. A workaround for this is to use a class method.
String refs are a legacy feature and they are likely to be removed in future React versions.
The way it works is that you provide a string as a ref value like ref="exampleRef"
and it automatically gets assigned to this.refs
.
Note: String refs can only be used with class components.
Here's an usage example:
export default class App extends React.Component {
render() {
console.log(this.refs);
return (
<div ref="exampleRef">
<button onClick={() => this.setState({ dummy: 0 })}>Re-render</button>
</div>
);
}
}
Here's the value for this.refs
across renders:
Render | this.refs |
---|---|
1 | {} |
2 | { exampleRef: <div>...</div> } |
3 | { exampleRef: <div>...</div> } |
4 | { exampleRef: <div>...</div> } |
As you can see, on the first render this.refs.exampleRef
will be undefined and
on the following renders it will point out to the specified DOM node.
We saw what useRef
is, how it differentiates with a plain old variable and
state variables, and we saw real world examples that uses it. I hope that most
of the content makes sense to you!
I'd love to hear your feedback. You can reach out to me on Twitter at any time :-)