Editor’s note: This post was updated on 23 February 2023 to add interactive code examples, update any outdated information, and include use cases for React refs. Check out our React Hooks cheat sheet to learn more about React Hooks.
As with many other UI libraries, React offers a way to rethink a view as the result of a component’s state. This is a significant pivot away from how we usually build applications. When we become familiar with some of these concepts, we discover how easy it is to solve simple problems in the frontend world that used to cause us some trouble. Part of that benefit comes from creating views with the abstraction mechanisms that React and JSX expose instead of doing it through DOM spec methods.
Still, the React team did something clever: they provided escape hatches. They kept the library open for situations beyond the ones they were specifically designing for and situations the model may not work for. We’ll cover anti-patterns later in this article.
These escape hatches are refs, which allow us to access DOM properties directly. Normally, React uses state to update the data on the screen by re-rendering the component for us. But, there are certain situations where you need to deal with the DOM properties directly, and that’s where refs come in clutch.
An example would be auto-focusing a text box when a component renders. React doesn’t provide an easy way to do this, so we can use refs to access the DOM directly and focus the text box whenever the component renders on the screen.
In this article, we will investigate why React, a framework meant to abstract your code away from DOM manipulation, leaves the door open for developers to access it.
Jump ahead:
Creating refs
When working with class-based components in the past, we used createRef()
to create a ref. However, now that React recommends functional components and general practice is to follow the Hooks way of doing things, we don’t need to use createRef()
. Instead, we use useRef(null)
to create refs in functional components.
As stated in the intro, refs are escape hatches for React developers, and we should try to avoid using them if possible. When we obtain a node using a ref
and later modify some attribute or the DOM structure of it, it can enter into conflict with React’s diff and update approaches.
First, let’s start with a simple component and grab a node element using refs:
import React from "react"; const ActionButton = ({ label, action }) => { return <button onClick={action}>{label}</button>; }; export default ActionButton;
The <button>
expression here is the JSX way of calling the React.createElement('button')
statement, which is not a representation of an HTML button element — it’s a React element.
You can gain access to the actual HTML element by creating a React reference and passing it to the element itself:
import React, { useRef } from "react"; const ActionButton = ({ label, action }) => { const buttonRef = useRef(null); return ( <button onClick={action} ref={buttonRef}> {label} </button> ); }; export default ActionButton;
This way, at any time in the component’s lifecycle, we can access the actual HTML element at buttonRef.current
. Now, we know how to access DOM nodes inside a React component. Let’s take a look at some of the situations where this may be useful.
Differences between useRef
and createRef
The first difference between useRef
and createRef
is that createRef
is typically used when creating a ref in a class component while useRef
is used in function components. Additionally, createRef
returns a new ref object each time it is called while useRef
returns the same ref object on every render.
Another main difference is that createRef
doesn’t accept an initial value, so the current
property of the ref will be initially set to null
. On the other hand, useRef
can accept an initial value and the current
property of the ref will be set to that value.
Using React refs
One of the many concepts that React popularized among developers is the concept of declarative views. Before declarative views, most of us modified the DOM by calling functions that explicitly changed it. As mentioned in the introduction of this article, we are now declaring views based on a state, and — though we are still calling functions to alter this state — we are not in control of when the DOM will change or even if it should change.
Because of this inversion of control, we’d lose this imperative nature if it weren’t for refs. Here are a few use cases where bringing refs into your code may make sense.
Focus control
You can achieve focus in an element programmatically by calling focus()
on the node instance. Because the DOM exposes this as a function call, the best way to do this in React is to create a ref and manually do it when we think it’s suitable, as shown below:
import React, { useState } from "react"; const InputModal = ({ initialValue }) => { const [value, setValue] = useState(initialValue); const onChange = (e) => { setValue(e.target.value); }; const onSubmit = (e) => { e.preventDefault(); }; return ( <div className="modal--overlay"> <div className="modal"> <h1>Insert a new value</h1> <form action="?" onSubmit={onSubmit}> <input type="text" onChange={onChange} value={value} /> <button>Save new value</button> </form> </div> </div> ); }; export default InputModal;
In this modal, we allow the user to modify a value already set in the screen below. It would be a better UX if the input was on focus when the modal opens, which would enable a smooth keyboard transition between the two screens. The first thing we need to do is get a reference for the input
:
import React, { useRef, useState } from "react"; const InputModal = ({ initialValue }) => { const [value, setValue] = useState(initialValue); const inputRef = useRef(null); const onChange = (e) => { setValue(e.target.value); }; const onSubmit = (e) => { e.preventDefault(); }; return ( <div className="modal--overlay"> <div className="modal"> <h1>Insert a new value</h1> <form action="?" onSubmit={onSubmit}> <input ref={inputRef} type="text" onChange={onChange} value={value} /> <button>Save new value</button> </form> </div> </div> ); }; export default InputModal;
Next, when our modal
mounts, we imperatively call focus
on our input ref
within a useEffect
:
import React, { useEffect, useRef, useState } from "react"; const InputModal = ({ initialValue }) => { const [value, setValue] = useState(initialValue); const inputRef = useRef(null); useEffect(() => { inputRef.current.focus(); }, []) const onChange = (e) => { setValue(e.target.value); }; const onSubmit = (e) => { e.preventDefault(); }; return ( <div className="modal--overlay"> <div className="modal"> <h1>Insert a new value</h1> <form action="?" onSubmit={onSubmit}> <input ref={inputRef} type="text" onChange={onChange} value={value} /> <button>Save new value</button> </form> </div> </div> ); }; export default InputModal;
So, when you open the modal
, you should see the text box focused by default:
Remember that you need to access the element through the current
property.
Here’s a CodeSandbox for the example above:
Detect if an element is contained
Similarly, you want to know if any element dispatching an event should trigger some action on your app. For example, our modal
component could be closed if the user clicked outside of it, like so:
import React, { useEffect, useRef, useState } from "react"; const InputModal = ({ initialValue, onClose, onSubmit }) => { const [value, setValue] = useState(initialValue); const inputRef = useRef(null); const modalRef = useRef(null); useEffect(() => { inputRef.current.focus(); document.body.addEventListener("click", onClickOutside); return () => document.removeEventListener("click", onClickOutside); }, []); const onClickOutside = (e) => { const element = e.target; if (modalRef.current && !modalRef.current.contains(element)) { e.preventDefault(); e.stopPropagation(); onClose(); } }; const onChange = (e) => { setValue(e.target.value); }; const onSub = (e) => { e.preventDefault(); onSubmit(value); onClose(); }; return ( <div className="modal--overlay"> <div className="modal" ref={modalRef}> <h1>Insert a new value</h1> <form action="?" onSubmit={onSub}> <input ref={inputRef} type="text" onChange={onChange} value={value} /> <button>Save new value</button> </form> </div> </div> ); }; export default InputModal;
Here, we check if the element click
is outside the modal
limits. It will work like this:
If it is, then we are preventing further actions and calling the onClose
callback because the modal
component expects to be controlled by its parent. Remember to check if the DOM element’s current reference still exists, as state changes in React are asynchronous. To achieve this, we are adding a global click listener on the body
element. It’s important to remember to clean the listener
when the element is unmounted.
Integrating with DOM-based libraries
As good as React is, many utilities and libraries outside its ecosystem have been in use on the web for years. For example, using refs allows us to combine React with a great animation library. It’s good to take advantage of their stability and resolution for some specific problems.
The GSAP library is a popular choice for animation examples. To use it, we need to send a DOM
element to any of its methods. Let’s go back to our modal and add some animations to make its appearance fancier:
import React, { useEffect, useRef, useState } from "react"; import gsap from "gsap"; const InputModal = ({ initialValue, onClose, onSubmit }) => { const [value, setValue] = useState(initialValue); const inputRef = useRef(null); const modalRef = useRef(null); const overlayRef = useRef(null); const onComplete = () => { inputRef.current.focus(); }; const timeline = gsap.timeline({ paused: true, onComplete }); useEffect(() => { timeline .from(overlayRef.current, { duration: 0.25, autoAlpha: 0, }) .from(modalRef.current, { duration: 0.25, autoAlpha: 0, y: 25, }); timeline.play(); document.body.addEventListener("click", onClickOutside); return () => { timeline.kill(); document.removeEventListener("click", onClickOutside); }; }, []); const onClickOutside = (e) => { const element = e.target; if (modalRef.current && !modalRef.current.contains(element)) { e.preventDefault(); e.stopPropagation(); onClose(); } }; const onChange = (e) => { setValue(e.target.value); }; const onSub = (e) => { e.preventDefault(); onSubmit(value); onClose(); }; return ( <div className="modal--overlay" ref={overlayRef}> <div className="modal" ref={modalRef}> <h1>Insert a new value</h1> <form action="?" onSubmit={onSub}> <input ref={inputRef} type="text" onChange={onChange} value={value} /> <button>Save new value</button> </form> </div> </div> ); }; export default InputModal;
At the constructor level, we are setting up the initial animation values, which will modify the styles of our DOM references. The timeline only plays when the component mounts. When the element is unmounted, we’ll clean the DOM state and actions by terminating any ongoing animation with the kill()
method supplied by the Timeline
instance.
Now, we’ll turn our focus to the input
after the timeline
has been completed.
When should you use useRef
Hook?
There are some instances where you would want to use the useRef
Hook, including the following:
- Accessing DOM elements: You can use the
useRef
Hook when you need to interact with a specific DOM element in your component, such as setting the focus on an input field or measuring an element’s size - Storing values that don’t trigger re-renders: When you have a value that changes frequently but doesn’t trigger a re-render, you can use
useRef
to store that value. For example, if you have a timer in your component, you could useuseRef
to store the current time without triggering a re-render - Caching expensive computations: If you need to avoid repeating an expensive computation on every render, you can use
useRef
to store the result of that computation
Avoiding React ref anti-patterns
Once you know how refs work, it’s easy to use them where they’re not needed. There’s more than one way to achieve the same thing inside a React component, so it’s easy to fall into an anti-pattern. My rule when it comes to ref usage is this: only use a ref when you need to imperatively call a function for a behavior React doesn’t allow you to control.
A simpler way to put that is when you need to call a function, and that function has no association with a React method or artifact, use a ref. Let’s explore an anti-pattern that I’ve seen repeatedly:
import React, { useRef } from "react"; const Form = () => { const [storedValue, setStoredValue] = useState(""); const inputRef = useRef(null); const onSubmit = (e) => { e.preventDefault(); setStoredValue(inputRef.current.value); }; return ( <div className="modal"> <form action="?" onSubmit={onSubmit}> <input ref={inputRef} type="text" /> <button>Submit</button> </form> </div> ); };
It’s fair to say if you want to send a value
on submit
, this approach will work, but the issue here is that because we know refs provide an escape hatch of the view model React offers, we can too easily go sniffing into DOM element values or properties that we can access through React’s interface. Controlling the input
value means we can always check its value. We don’t need to use refs here to access the value of the text box. We can use the value provided by React itself:
return ( <input type="text" onChange={e => setValue(e.target.value)} value={value} /> )
Let’s go back to our rule: only use a ref when you need to imperatively call a function for a behavior React doesn’t allow you to control. In our uncontrolled input
we create a ref but don’t make an imperative call. Then, that function should exist, which is not satisfied as I can indeed control an input’s value.
Using forwardRef
As we’ve discussed, refs are useful for really specific actions. The examples shown are a little simpler than what we usually find in a web application codebase nowadays. Components are more complex, and we barely use plain HTML elements directly. It’s really common to include more than one node to encapsulate more logic around the view behavior. Here’s an example:
import React from 'react' const LabelledInput = (props) => { const { id, label, value, onChange } = props return ( <div class="labelled--input"> <label for={id}>{label}</label> <input id={id} onChange={onChange} value={value} /> </div> ) } export default LabelledInput
The issue now is that passing a ref to this component will return its instance, a React component reference, and not the input
element we want to focus on, like in our first example. Luckily, React provides an inbuilt solution for this called forwardRef
, which allows you to define internally what element the ref
will point at:
import React from 'react' const LabelledInput = (props, ref) => { const { id, label, value, onChange } = props return ( <div class="labelled--input"> <label for={id}>{label}</label> <input id={id} onChange={onChange} value={value} ref={ref}/> </div> ) } export default React.forwardRef(LabelledInput)
See this example in action:
To achieve this, we’ll pass a second argument to our function and place it in the desired element. Now, when a parent component passes a ref
value, it will obtain the input
, which is helpful to avoid exposing the internals and properties of a component and breaking its encapsulation. The example of our form that we saw failing to achieve focus will now work as expected.
Conclusion
We started with a recap on the basic concepts of React and its usage, why we generally shouldn’t break the framework’s model, and why we may sometimes need to. Accessing the DOM through the interface the library exposes helps to maintain the internals of React in place (remember that useState
contains more logic than just triggering a re-render cycle, like batching updates and, in the future, time slicing).
Breaking this model with anti-patterns can render later performance improvements in the library useless, or even create bugs in your applications. Remember to only use refs when there is an implicit function call that React can’t handle through its methods. Also, make sure it doesn’t alter the internal state of the components. For more information, read the official React documentation about refs.
Thanks for this article, Jeremias. I use `useRef` and `createRef` extensively with my React code, which is entirely function, not class based, and extensively using Hooks. I’ve examined some `forwardRef` code and found it insanely complex and confusing so just used `window.###` functions instead.
Awesome read, appreciate it man.
Little typo in the last sample, should be
export default React.forwardRef(LabelledInput)
👍 Thanks for the catch
The forwardRef brought me to this article. The example are nice. The problem with forwardRef is that it causes a child component to re-render even if the props aren’t changed.
I’m trying to fix this issue myself. Your example has this problem.
Here’s my fork of your example: https://codesandbox.io/s/input-modal-example-re-render-issue-luuu8?file=/src/input-modal.js
Thanks a lot.
Good Info, its more helpful
about first Application scenario,why we don’t use element’s attribute,that’s easy more and useful more, “autofocus=true”
Using a ref can give you more fine-grained control over when and how the input field is focused.