What are refs?
If you read my last article, about the differences between useEffect
and useLayoutEffect
, you may remember seeing some code snippets that looked like this:
useEffect(() => {
const greenSquare = document.querySelector(".App__square")
greenSquare.style.transform = "translate(-50%, -50%)"
greenSquare.style.left = "50%"
greenSquare.style.top = "50%"
})
useLayoutEffect(() => {
const greenSquare = document.querySelector(".App__square")
greenSquare.style.transform = "translate(-50%, -50%)"
greenSquare.style.left = "50%"
greenSquare.style.top = "50%"
})
In these examples, we're directly accessing the DOM in order to select and manipulate an element (i.e. .App__square
), which is considered an anti-pattern in React because it manages UI state via a virtual DOM and comparing it to the browser's version. Then, the framework handles the work of reconciling the two. However, there are cases where we need may need to break this rule. That's where refs
come in.
While the React docs cite a few examples where using refs
would be appropriate, including managing focus, triggering animations, and working with third party libraries, they also warn against overusing them.
Avoid using refs for anything that can be done declaratively. ~ React docs
For a practical example of how to use refs
in your React app, checkout my previous article about rebuilding a search UI using refs
and React Context. We'll also cover the ins and outs of Context in the next article in this series.
In the next section, we'll look more closely at the useRef
hook and its syntax.
Anatomy of useRef
...useRef is like a “box” that can hold a mutable value... ~ React docs
The useRef
hook only takes one argument: its initial value. This can be any valid JavaScript value or JSX element. Here are a few examples:
// String value
const stringRef = useRef("initial value")
// Array value
const arrayRef = useRef([1, 2, 3])
// Object value
const objectRef = useRef({
firstName: "Ryan",
lastName: "Harris",
})
Essentially, you can store any value in your ref
and then access it via the ref
's current
field. For example, if we logged out the variables from the snippet above, we would see:
console.log(stringRef)
// {
// current: "initial value"
// }
console.log(arrayRef)
// {
// current: [1, 2, 3]
// }
console.log(objectRef)
// {
// current: {
// firstName: 'Ryan',
// lastName: 'Harris'
// }
// }
As I mentioned in the intro, refs
are primarily used for accessing the DOM. Below is an example of how you would define and use a ref
in the context of a class
component:
class MyComponent extends React.Component {
constructor() {
super();
this.inputRef = React.createRef();
}
render() {
return (
<div className="App">
<input ref={this.inputRef} type="text" />
</div>
);
}
}
To do the same exact thing using hooks, we would leverage useRef
like you see in the snippet below:
function MyComponent() {
const inputRef = useRef(null);
return (
<div className="App">
<input ref={inputRef} type="text" />
</div>
);
}
Hopefully, those examples clearly illustrated how to define a ref. Just remember: refs
are a "reference" to a DOM element -- it's right in the name!
refs
also have another less-known use case. Since a ref
's value can be any JavaScript value, you can also use refs
as basic data stores. Usually, you would use useState
for something like that, however, there are time where you want to avoid uneccessary re-renders but cache a value. Updating values in state cause a re-render each time, whereas updating refs
do not cause the component to update. This is a subtle, but important distinction.
In practice
In the sections below, we'll walk through two examples that better illustrate how to use useRef
both to access DOM elements and store values without causing our component to re-render.
Accessing DOM elements
For this example, I have built a small SearchInput
component that uses the useRef
hook in order to refer to the <input />
element rendered by our component:
In this specific case, our SearchInput
component takes an autoFocus
prop, which determines whether or not we want the <input />
to be focused automatically on mount. In order to do this, we need to use a web API (i.e. .focus()
) and thus need to be able to directly refer to the HTML element on the page.
To get this to work, the first thing we need to do is create a ref
and assign it to our element:
// This instantiates our ref
const inputRef = useRef(null);
// Inside our return, we point `inputRef` at our <input /> element
<input ref={inputRef} type="search" className="SearchInput__input" />
Now, our inputRef
is pointing at the search input, so if we were to log out inputRef.current
, we would see our <input />
:
console.log(inputRef.current)
// <input type="search" class="SearchInput__input"></input>
With this wired up, we can now autofocus the input on mount, as well as add some styling to make our SearchInput
component look more cohesive even though it is made up of multiple elements "under the hood". In order to handle the autofocus behavior, we need to use the useLayoutEffect
hook to focus the input prior to the DOM painting.
Note: For more info on when to use useLayoutEffect
vs. useEffect
, check out my previous article in this series.
useLayoutEffect(() => {
if (autoFocus) {
inputRef.current.focus();
setFocused(true);
}
}, [autoFocus]);
By calling inputRef.current.focus()
, we are setting the <input />
inside our component as the active element in the document. In addition, we're also updating our focused
value stored in a useState
hook in order to style our component.
const focusCn = focused ? "SearchInput focused" : "SearchInput";
Finally, I have also added an event listener using a useEffect
hook in order to update our focus state based on mouse clicks both inside and outside of our component. Essentially, when the user clicks inside SearchInput
, we call .focus()
and update our focused
state to true
. Alternatively, when the user clicks outside the component, we call .blur()
and set focused
to false
.
useEffect(() => {
function handleClick(event) {
if (event.target === inputRef.current) {
inputRef.current.focus();
setFocused(true);
} else {
inputRef.current.blur();
setFocused(false);
}
}
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
});
While accessing DOM elements is a React anti-pattern (as discussed above), this example is a valid use case for refs
because our goal requires the use of .focus()
, which is only available to HTML elements.
Storing values without re-rendering
In this example, I want to illustrate the subtle difference between using useState
and useRef
to store values.
Here, we have two sections that have buttons, which allow us to increment/decrement our refValue
or stateValue
, respectively. When the page initally loads, each section is assigned a random hex value as its background-color
. From then on, you will see the colors change whenever our App
component re-renders.
Since updating state values cause a re-render, you should see the stateValue
number update every time you click on of the buttons; however, if you click on the buttons for our refValue
, nothing happens. This is because updating ref
values does not cause a component to re-render. To demonstrate that the refValue
is in fact changing, I have added console.log
statements to the onClick
handlers for both buttons.
While incrementing or decrementing the refValue
will not cause our UI to update with the proper numeric value, when you change the stateValue
our refValue
will update and its section will have a new background color. This is because our ref
section is re-rendered when the state value is updated since the parent component App
has to go through reconciliation to bring the virtual DOM and browser DOM into sync with one another. This can be a great strategy for avoiding uneccessary renders in your application and improving its performance!
Top comments (0)