By default, React will be handling all the DOM manipulation automatically, and you only need to describe what the UI should look like. But in some cases, you might need to access and manipulate the DOM manually, such as accessing the size of an element, implementing drag and drop, managing scroll positions, and so on.
Refs, short for references, are used to directly access and manipulate DOM elements or components without using the standard React state and props system.
Declaring refs
Refs are created with the useRef()
hook, which can be imported like this:
1import { useRef } from "react";
Inside your component, declare a ref like this:
1export default function App() {
2 const divRef = useRef(null);
3
4 return (. . .);
5}
useRef()
returns an object with a current
property, which is supposed to point to a DOM node. But for now, we are initializing it to be null
.
Tieing refs to DOM nodes
You can then assign a DOM node to our divRef.current
by specifying a ref
attribute.
1import { useRef } from "react";
2
3export default function App() {
4 const divRef = useRef(null);
5
6 return <div ref={divRef}>Lorem ipsum dolor. . .</div>;
7}
This will tell React to tie this <div>
element to divRef.current
. You can now access the associated DOM element and its properties directly via the standard web APIs like this:
1import { useRef, useState } from "react";
2
3export default function App() {
4 const divRef = useRef(null);
5 const [height, setHeight] = useState(0);
6
7 return (
8 <>
9 <div ref={divRef}>Lorem ipsum dolor. . .</div>
10
11 <button
12 onClick={() => {
13 setHeight(divRef.current.clientHeight);
14 }}>
15 Calculate Height
16 </button>
17
18 <div>{height}</div>
19 </>
20 );
21}
In this example, clientHeight
returns the height of an element. When you click on the button, the corresponding event handler will update height
with divRef.current.clientHeight
.
Refs are attached after rendering
One important thing to note about refs is that React only attaches the DOM node after the DOM tree has been rendered, which means when you try to access the ref before or during the rendering phase, that ref will be assigned null
, and will likely cause an error in your code. For instance,
1import { useRef } from "react";
2
3export default function App() {
4 const divRef = useRef(null);
5
6 console.log(divRef.current.clientHeight); // This line will cause an error
7
8 return <div ref={divRef}>Lorem ipsum dolor. . .</div>;
9}
In this example, we are trying to access clientHeight
before the <div>
element is rendered. divRef.current
will be assigned null
at the time, so an error will be returned.
This is why in most cases, you will be dealing with refs inside event handlers, which are also only attached after rendering. If you need to do something with ref, but there is no associated event handler, you will need to use side effects, which we are going to discuss in the next lesson.
Best practices for refs
Refs is a way to step outside of scope of React, giving you direct access to the DOM tree. It can be a powerful tool for interacting with the DOM, but it is essential for you to understand its proper role in a React application, and use it with caution.
When using refs, there are several best practice guidelines you should follow.
First of all, do not rely on refs. One of React’s core strengths is its declarative nature, where you can describe what the UI should look like, and React takes care of DOM manipulation for you. Excessive use of refs can break this declarative model and make your code more difficult to manage and debug.
And second, you should only access refs after the component has been rendered. As discussed above, React only attaches the DOM node to the corresponding ref after the component has been rendered. Attempting to access a ref before that will return null
. To be safe, you should place ref related logic inside event handlers or side effects.
Lastly, although refs can store data across renders like like states, you should avoid using refs for state management. Consider the following example,
1import { useRef } from "react";
2
3export default function App() {
4 const count = useRef(0);
5
6 return (
7 <div>
8 <p>{count.current}</p>
9 <button
10 onClick={() => {
11 count.current = count.current + 1;
12 console.log(count.current);
13 }}>
14 Add
15 </button>
16 </div>
17 );
18}
Every time the button is clicked, count.current
will increment by 1, as you can see logged in the console. However, notice that the counter displayed on the webpage will not be updated because changes to count.current
do not trigger re-render.