A Complete Guide to useRef
What's useRef
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 😄):
Interactive Example
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.
Difference with a variable
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:
Interactive Example
TypeResultoutside variable
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.
Interactive Example
TypeResultoutside variableoutside variableoutside variable
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:
Interactive Example
TypeResultrefrefref
Difference with useState
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.
Interactive Example
TypeResultstateref
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.
Refs as a Way to Access Underlying DOM Elements
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:
React
Hey, what's up?
12:00Could you give me a reference to this dom node?
12:00React
Sure, I assigned it to the 'current' property of your ref.
12:00On 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:
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.
Real World Use Cases for Refs
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)
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:
Interactive Example
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
})
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:
function usePrevious(value) {
const ref = useRef()
return ref.current
}
On the first render it is called with a value of 0
:
const previousClicks = usePrevious(0)
In this case, usePrevious
will return undefined
:
function usePrevious(value) {
const ref = useRef()
return ref.current
}
After increase the value for count, here's how the usePrevious
call will look:
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:
function usePrevious(value) {
const ref = useRef()
return ref.current
}
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
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 [rerenders, setRerenders] = useState(0);
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.
(Legacy) String Refs
Warning
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.
Conclusion
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 :-)
from Hacker News https://ift.tt/3agfxYk
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.